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

yyle88 / osexec / 20189108589

13 Dec 2025 07:54AM UTC coverage: 35.088% (-2.5%) from 37.578%
20189108589

push

github

yangyile1990
Fix Warp typo to Wrap and reorganize code structure

- Fix typo: WarpOutputs→WrapOutputs, WarpMessage→WrapMessage, WrapResults, WrapOutcome
- Split utils.go into alias.go and debug.go
- Remove redundant aliases (CMC, OsCommand), retain just ExecConfig
- Use syntaxgo_reflect.GetPkgName() to get package name in sure.gen_test.go
- Update dependencies in go.mod/go.sum
- Standardize test packages to osexec_test naming convention

14 of 28 new or added lines in 5 files covered. (50.0%)

20 existing lines in 3 files now uncovered.

280 of 798 relevant lines covered (35.09%)

8.53 hits per line

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

74.58
/command.go
1
package osexec
2

3
import (
4
        "bufio"
5
        "io"
6
        "os"
7
        "os/exec"
8
        "slices"
9
        "strings"
10
        "sync"
11

12
        "github.com/yyle88/done"
13
        "github.com/yyle88/erero"
14
        "github.com/yyle88/eroticgo"
15
        "github.com/yyle88/osexec/internal/utils"
16
        "github.com/yyle88/printgo"
17
        "github.com/yyle88/tern"
18
        "github.com/yyle88/zaplog"
19
        "go.uber.org/zap"
20
)
21

22
// CommandConfig represents the configuration when executing shell commands
23
// CommandConfig 表示执行 shell 命令时的配置
24
type CommandConfig struct {
25
        Envs      []string                     // Custom environment variables // 自定义环境变量
26
        Path      string                       // Execution path // 执行路径
27
        ShellType string                       // Type of shell to use, e.g., bash, zsh // shell 类型,例如 bash,zsh
28
        ShellFlag string                       // Shell flag, e.g., "-c" // Shell 参数,例如 "-c"
29
        DebugMode DebugMode                    // Debug mode setting // 调试模式设置
30
        MatchPipe func(outputLine string) bool // Function to match output lines in pipe mode // 管道模式下匹配输出行的函数
31
        MatchMore bool                         // Continue matching even when matched // 即使匹配成功也继续匹配
32
        TakeExits map[int]string               // Map of expected exit codes with reasons // 预期退出码及其原因的映射表
33
}
34

35
// NewCommandConfig creates and returns a new CommandConfig instance.
36
// NewCommandConfig 创建并返回一个新的 CommandConfig 实例。
37
func NewCommandConfig() *CommandConfig {
90✔
38
        return &CommandConfig{
90✔
39
                DebugMode: NewDebugMode(debugModeOpen), // consistent with the debugModeOpen variable. // 初始值与 debugModeOpen 保持一致。
90✔
40
                MatchPipe: func(outputLine string) bool { return false },
105✔
41
                TakeExits: make(map[int]string),
42
        }
43
}
44

45
// WithEnvs sets the environment variables and returns the updated instance
46
// WithEnvs 设置环境变量并返回更新后的实例
47
func (c *CommandConfig) WithEnvs(envs []string) *CommandConfig {
20✔
48
        c.Envs = envs
20✔
49
        return c
20✔
50
}
20✔
51

52
// WithPath sets the execution path and returns the updated instance
53
// WithPath 设置执行路径并返回更新后的实例
54
func (c *CommandConfig) WithPath(path string) *CommandConfig {
25✔
55
        c.Path = path
25✔
56
        return c
25✔
57
}
25✔
58

59
// WithShellType sets the shell type and returns the updated instance
60
// WithShellType 设置 shell 类型并返回更新后的实例
61
func (c *CommandConfig) WithShellType(shellType string) *CommandConfig {
5✔
62
        c.ShellType = shellType
5✔
63
        return c
5✔
64
}
5✔
65

66
// WithShellFlag sets the shell flag and returns the updated instance
67
// WithShellFlag 设置 shell 参数并返回更新后的实例
68
func (c *CommandConfig) WithShellFlag(shellFlag string) *CommandConfig {
5✔
69
        c.ShellFlag = shellFlag
5✔
70
        return c
5✔
71
}
5✔
72

73
// WithShell sets both the shell type and shell flag, returns the updated instance
74
// WithShell 同时设置 shell 类型和 shell 参数,返回更新后的实例
75
func (c *CommandConfig) WithShell(shellType, shellFlag string) *CommandConfig {
45✔
76
        c.ShellType = shellType
45✔
77
        c.ShellFlag = shellFlag
45✔
78
        return c
45✔
79
}
45✔
80

81
// WithBash sets the shell to bash with the "-c" flag and returns the updated instance.
82
// WithBash 设置 shell 为 bash 并附带 "-c" 参数,返回更新后的实例。
83
func (c *CommandConfig) WithBash() *CommandConfig {
15✔
84
        return c.WithShell("bash", "-c")
15✔
85
}
15✔
86

87
// WithZsh sets the shell to zsh with the "-c" flag and returns the updated instance.
88
// WithZsh 设置 shell 为 zsh 并附带 "-c" 参数,返回更新后的实例。
89
func (c *CommandConfig) WithZsh() *CommandConfig {
×
90
        return c.WithShell("zsh", "-c")
×
91
}
×
92

93
// WithSh sets the shell to sh with the "-c" flag and returns the updated instance.
94
// WithSh 设置 shell 为 sh 并附带 "-c" 参数,返回更新后的实例。
95
func (c *CommandConfig) WithSh() *CommandConfig {
20✔
96
        return c.WithShell("sh", "-c")
20✔
97
}
20✔
98

99
// WithDebug sets the debug mode to true and returns the updated instance
100
// WithDebug 将调试模式设置 true 并返回更新后的实例
101
func (c *CommandConfig) WithDebug() *CommandConfig {
10✔
102
        return c.WithDebugMode(DEBUG)
10✔
103
}
10✔
104

105
// WithDebugMode sets the debug mode and returns the updated instance
106
// WithDebugMode 设置调试模式并返回更新后的实例
107
func (c *CommandConfig) WithDebugMode(debugMode DebugMode) *CommandConfig {
45✔
108
        c.DebugMode = debugMode
45✔
109
        return c
45✔
110
}
45✔
111

112
// WithMatchPipe sets the match pipe function and returns the updated instance
113
// WithMatchPipe 设置匹配管道函数并返回更新后的实例
114
func (c *CommandConfig) WithMatchPipe(matchPipe func(outputLine string) bool) *CommandConfig {
×
115
        c.MatchPipe = matchPipe
×
116
        return c
×
117
}
×
118

119
// WithMatchMore sets the match more flag and returns the updated instance
120
// WithMatchMore 设置匹配更多标志并返回更新后的实例
121
func (c *CommandConfig) WithMatchMore(matchMore bool) *CommandConfig {
×
122
        c.MatchMore = matchMore
×
123
        return c
×
124
}
×
125

126
// WithTakeExits sets the accepted exit codes and returns the updated instance
127
// WithTakeExits 设置接受退出码集合并返回更新后的实例
128
func (c *CommandConfig) WithTakeExits(takeExits map[int]string) *CommandConfig {
5✔
129
        // Clone map to avoid shared reference issues, and avoid using maps.Clone when input could be nil
5✔
130
        // 复制 map 避免共享引用问题,不使用 maps.Clone 以防输入为 nil
5✔
131
        expMap := make(map[int]string, len(takeExits))
5✔
132
        for k, v := range takeExits {
10✔
133
                expMap[k] = v
5✔
134
        }
5✔
135
        // Replace instead of merge, as replacement matches expected pattern
136
        // 完全替换而非合并,因为替换更符合预期模式
137
        c.TakeExits = expMap
5✔
138
        return c
5✔
139
}
140

141
// WithExpectExit adds an expected exit code and returns the updated instance
142
// WithExpectExit 添加期望的退出码并返回更新后的实例
143
func (c *CommandConfig) WithExpectExit(exitCode int, reason string) *CommandConfig {
5✔
144
        c.TakeExits[exitCode] = reason
5✔
145
        return c
5✔
146
}
5✔
147

148
// WithExpectCode adds an expected exit code and returns the updated instance
149
// WithExpectCode 添加期望的退出码并返回更新后的实例
150
func (c *CommandConfig) WithExpectCode(exitCode int) *CommandConfig {
15✔
151
        c.TakeExits[exitCode] = "EXPECTED-EXIT-CODES"
15✔
152
        return c
15✔
153
}
15✔
154

155
// Exec executes a shell command with the specified name and arguments, using the CommandConfig configuration.
156
// Exec 使用 CommandConfig 的配置执行带有指定名称和参数的 shell 命令。
157
func (c *CommandConfig) Exec(name string, args ...string) ([]byte, error) {
70✔
158
        const skipDepth = 1
70✔
159

70✔
160
        if err := c.checkConfig(name, args, skipDepth+1); err != nil {
70✔
161
                return nil, erero.Ero(err)
×
162
        }
×
163
        command := c.prepareCommand(name, args)
70✔
164
        return utils.WrapResults(done.VAE(command.CombinedOutput()), c.IsShowOutputs(), c.TakeExits)
70✔
165
}
166

167
// ExecWith executes a command with custom exec.Cmd preparation
168
// Allows setting stdin, extra env vars, and additional cmd fields via prepare callback
169
//
170
// ExecWith 执行命令,支持自定义 exec.Cmd 配置
171
// 通过 prepare 回调可设置 stdin、额外环境变量和其它 cmd 字段
172
func (c *CommandConfig) ExecWith(name string, args []string, prepare func(command *exec.Cmd)) ([]byte, error) {
5✔
173
        const skipDepth = 1
5✔
174

5✔
175
        if err := c.checkConfig(name, args, skipDepth+1); err != nil {
5✔
176
                return nil, erero.Ero(err)
×
177
        }
×
178
        command := c.prepareCommand(name, args)
5✔
179
        prepare(command)
5✔
180
        return utils.WrapResults(done.VAE(command.CombinedOutput()), c.IsShowOutputs(), c.TakeExits)
5✔
181
}
182

183
// ExecTake executes a command and returns output, exit code, and an issue if one exists
184
// Returns exit code enabling fine-grained handling of command outcomes
185
// Exit code 0 indicates success, non-zero indicates various conditions
186
//
187
// ExecTake 执行命令并返回输出、退出码和错误(如果有的话)
188
// 返回退出码以便精细处理命令结果
189
// 退出码 0 表示成功,非零表示各种情况
190
func (c *CommandConfig) ExecTake(name string, args ...string) ([]byte, int, error) {
10✔
191
        const skipDepth = 1
10✔
192

10✔
193
        if err := c.checkConfig(name, args, skipDepth+1); err != nil {
10✔
194
                return nil, -1, erero.Ero(err)
×
195
        }
×
196
        command := c.prepareCommand(name, args)
10✔
197
        return utils.WrapOutcome(done.VAE(command.CombinedOutput()), c.IsShowOutputs(), c.TakeExits)
10✔
198
}
199

200
// IsShowCommand checks if the command should be displayed based on the debug mode
201
// IsShowCommand 检查是否应根据调试模式显示命令
202
func (c *CommandConfig) IsShowCommand() bool {
90✔
203
        return isShowCommand(c.DebugMode)
90✔
204
}
90✔
205

206
// IsShowOutputs checks if the command results should be displayed based on the debug mode
207
// IsShowOutputs 检查是否应根据调试模式显示命令结果
208
func (c *CommandConfig) IsShowOutputs() bool {
105✔
209
        return isShowOutputs(c.DebugMode)
105✔
210
}
105✔
211

212
// checkConfig validates the command configuration and shows debug info if needed.
213
// checkConfig 验证命令配置并在需要时显示调试信息。
214
func (c *CommandConfig) checkConfig(name string, args []string, skipDepth int) error {
90✔
215
        if name == "" {
90✔
216
                return erero.New("can-not-execute-with-blank-command-name")
×
217
        }
×
218
        if c.ShellFlag == "" && c.ShellType == "" {
130✔
219
                if strings.Contains(name, " ") {
40✔
220
                        return erero.New("can-not-contains-space-in-command-name")
×
221
                }
×
222
        }
223
        if c.ShellFlag != "" {
140✔
224
                if c.ShellType == "" {
50✔
225
                        return erero.New("can-not-execute-with-wrong-shell-command")
×
226
                }
×
227
        }
228
        if c.ShellType != "" {
140✔
229
                if c.ShellFlag != "-c" {
50✔
230
                        return erero.New("can-not-execute-with-wrong-shell-options")
×
231
                }
×
232
        }
233
        if c.IsShowCommand() {
125✔
234
                debugMessage := c.makeCommandMessage(name, args)
35✔
235
                utils.ShowCommand(debugMessage)
35✔
236
                zaplog.ZAPS.Skip(skipDepth).LOG.Debug("EXEC:", zap.String("CMD", debugMessage))
35✔
237
        }
35✔
238
        return nil
90✔
239
}
240

241
// prepareCommand creates and configures an exec.Cmd based on the CommandConfig settings.
242
// prepareCommand 根据 CommandConfig 设置创建并配置 exec.Cmd。
243
func (c *CommandConfig) prepareCommand(name string, args []string) *exec.Cmd {
90✔
244
        cmd := tern.BFF(c.ShellType != "",
90✔
245
                func() *exec.Cmd {
140✔
246
                        return exec.Command(c.ShellType, c.ShellFlag, name+" "+strings.Join(args, " "))
50✔
247
                },
50✔
248
                func() *exec.Cmd {
40✔
249
                        return exec.Command(name, args...)
40✔
250
                })
40✔
251
        cmd.Dir = c.Path
90✔
252
        // Set environment variables: when c.Envs has no items, Go uses os.Environ()
90✔
253
        // 设置环境变量:当 c.Envs 没有项目时,Go 使用 os.Environ()
90✔
254
        cmd.Env = tern.BF(len(c.Envs) > 0, func() []string {
110✔
255
                return append(os.Environ(), c.Envs...)
20✔
256
        })
20✔
257
        return cmd
90✔
258
}
259

260
// makeCommandMessage constructs a command-line string based on the CommandConfig and given command name and arguments.
261
// makeCommandMessage 根据 CommandConfig 和指定的命令名称及参数构造命令行字符串。
262
func (c *CommandConfig) makeCommandMessage(name string, args []string) string {
35✔
263
        var pts = printgo.NewPTS()
35✔
264
        if c.Path != "" {
45✔
265
                pts.Fprintf("cd %s && ", c.Path)
10✔
266
        }
10✔
267
        if len(c.Envs) > 0 {
40✔
268
                pts.Fprintf("%s ", strings.Join(c.Envs, " "))
5✔
269
        }
5✔
270
        if c.ShellType != "" && c.ShellFlag != "" {
55✔
271
                pts.Fprintf("%s %s '%s'", c.ShellType, c.ShellFlag, escapeSingleQuotes(makeCommandMessage(name, args)))
20✔
272
        } else {
35✔
273
                pts.Fprintf("%s %s", name, strings.Join(args, " "))
15✔
274
        }
15✔
275
        return pts.String()
35✔
276
}
277

278
// StreamExec executes a shell command with the specified name and arguments, using the CommandConfig configuration, and returns the output as a byte slice.
279
// StreamExec 使用 CommandConfig 的配置执行带有指定名称和参数的 shell 命令,并返回输出的字节切片。
280
func (c *CommandConfig) StreamExec(name string, args ...string) ([]byte, error) {
5✔
281
        return c.ExecInPipe(name, args...)
5✔
282
}
5✔
283

284
// ExecInPipe executes a shell command with the specified name and arguments, using the CommandConfig configuration, and returns the output as a byte slice.
285
// ExecInPipe 使用 CommandConfig 的配置执行带有指定名称和参数的 shell 命令,并返回输出的字节切片。
286
func (c *CommandConfig) ExecInPipe(name string, args ...string) ([]byte, error) {
5✔
287
        const skipDepth = 1
5✔
288

5✔
289
        if err := c.checkConfig(name, args, skipDepth+1); err != nil {
5✔
290
                return nil, erero.Ero(err)
×
291
        }
×
292
        command := c.prepareCommand(name, args)
5✔
293

5✔
294
        stdoutPipe, err := command.StdoutPipe()
5✔
295
        if err != nil {
5✔
296
                return nil, erero.Wro(err)
×
297
        }
×
298

299
        stderrPipe, err := command.StderrPipe()
5✔
300
        if err != nil {
5✔
301
                return nil, erero.Wro(err)
×
302
        }
×
303

304
        stdoutReader := bufio.NewReader(stdoutPipe)
5✔
305
        stderrReader := bufio.NewReader(stderrPipe)
5✔
306
        if err := command.Start(); err != nil {
5✔
307
                return nil, erero.Wro(err)
×
308
        }
×
309

310
        wg := sync.WaitGroup{}
5✔
311
        wg.Add(2)
5✔
312
        var errMatch = false
5✔
313
        var stderrBuffer = printgo.NewPTX()
5✔
314
        go func() {
10✔
315
                defer wg.Done()
5✔
316
                errMatch = c.readPipe(stderrReader, stderrBuffer, "REASON", eroticgo.RED)
5✔
317
        }()
5✔
318
        var outMatch = false
5✔
319
        var stdoutBuffer = printgo.NewPTX()
5✔
320
        go func() {
10✔
321
                defer wg.Done()
5✔
322
                outMatch = c.readPipe(stdoutReader, stdoutBuffer, "OUTPUT", eroticgo.GREEN)
5✔
323
        }()
5✔
324
        wg.Wait()
5✔
325

5✔
326
        // Wait for command to complete and get exit status
5✔
327
        // 等待命令完成并获取退出状态
5✔
328
        erw := command.Wait()
5✔
329

5✔
330
        // When output matched, exit with success status (can succeed even if erw != nil)
5✔
331
        // 当输出匹配成功时,以成功状态退出(即使 erw != nil 也可以成功)
5✔
332
        if outMatch {
5✔
NEW
333
                return utils.WrapResults(done.VAE(stdoutBuffer.Bytes(), erw), c.IsShowOutputs(), c.TakeExits)
×
334
        }
×
335

336
        // If stderr matched, return with stderr data (e.g., "go: upgraded xxx")
337
        // 如果 stderr 匹配成功,返回 stderr 数据(比如 "go: upgraded xxx")
338
        if errMatch {
5✔
NEW
339
                return utils.WrapResults(done.VAE(stderrBuffer.Bytes(), erw), c.IsShowOutputs(), c.TakeExits)
×
340
        }
×
341

342
        // No match found, check errors in sequence
343
        // 没有匹配,按顺序检查错误
344
        if erw != nil {
5✔
345
                // Command failed with non-zero exit code
×
346
                // 命令以非零退出码失败
×
NEW
347
                return utils.WrapResults(done.VAE(stdoutBuffer.Bytes(), erw), c.IsShowOutputs(), c.TakeExits)
×
348
        }
×
349

350
        if stderrBuffer.Len() > 0 {
5✔
351
                // Command succeeded but has stderr content
×
352
                // 命令成功但有 stderr 内容
×
NEW
353
                return utils.WrapMessage(done.VAE(stdoutBuffer.Bytes(), erero.New(stderrBuffer.String())), c.IsShowOutputs())
×
354
        }
×
355

356
        // Command succeeded with no errors
357
        // 命令成功且无错误
358
        return utils.WrapOutputs(stdoutBuffer.Bytes(), c.IsShowOutputs())
5✔
359
}
360

361
// readPipe reads from the provided reader and writes to the provided PTX buffer, using the specified debug message and colors.
362
// readPipe 从提供的 reader 读取数据并写入提供的 PTX 缓冲区,使用指定的调试消息和颜色。
363
func (c *CommandConfig) readPipe(reader *bufio.Reader, ptx *printgo.PTX, debugMessage string, erotic eroticgo.COLOR) (matched bool) {
10✔
364
        for {
25✔
365
                streamLine, _, err := reader.ReadLine()
15✔
366

15✔
367
                if c.IsShowOutputs() {
15✔
UNCOV
368
                        zaplog.SUG.Debugln(debugMessage, erotic.Sprint(string(streamLine)))
×
UNCOV
369
                }
×
370

371
                if (c.MatchMore || !matched) && c.MatchPipe != nil && c.MatchPipe(string(streamLine)) {
15✔
372
                        matched = true
×
373
                }
×
374

375
                if err != nil {
25✔
376
                        if err == io.EOF {
20✔
377
                                ptx.Write(streamLine)
10✔
378
                                return matched
10✔
379
                        }
10✔
380
                        panic(erero.Wro(err)) // Panic on failure for read error, which is rare // 读取错误时 panic,这种情况很罕见
×
381
                }
382
                ptx.Write(streamLine)
5✔
383
                ptx.Println()
5✔
384
        }
385
}
386

387
// NewConfig creates a shallow clone of the CommandConfig instance.
388
// NewConfig 克隆一个新的 CommandConfig 实例,以便于实现总配置和子配置分隔.
389
func (c *CommandConfig) NewConfig() *CommandConfig {
×
390
        return &CommandConfig{
×
391
                Envs:      slices.Clone(c.Envs),                          // Clone to avoid data sharing issues // 克隆以避免数据共享问题
×
392
                Path:      c.Path,                                        // Use same path // 使用相同路径
×
393
                ShellType: "",                                            // Each command sets its own // 各命令自行设置
×
394
                ShellFlag: "",                                            // Each command sets its own // 各命令自行设置
×
395
                DebugMode: c.DebugMode,                                   // Use same debug mode // 使用相同调试模式
×
396
                MatchPipe: func(outputLine string) bool { return false }, // Each command sets its own // 各命令自行设置
×
397
                MatchMore: false,                                         // Each command sets its own // 各命令自行设置
398
                TakeExits: make(map[int]string),                          // New map as different commands expect different exit codes // 新建映射表,因不同命令期望不同退出码
399
        }
400
}
401

402
// SubConfig creates a shallow clone of the CommandConfig instance with a new path and returns the updated instance.
403
// SubConfig 创建一个带有新路径的 CommandConfig 实例的浅克隆并返回更新后的实例。
404
func (c *CommandConfig) SubConfig(path string) *CommandConfig {
×
405
        return c.NewConfig().WithPath(path)
×
406
}
×
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