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

yyle88 / osexec / 19191100749

08 Nov 2025 09:19AM UTC coverage: 36.709% (-2.5%) from 39.204%
19191100749

push

github

yyle88
Enhance GitHub Actions workflow with secure scanning and code standards

- Add govulncheck scanning to test workflow
- Add CodeQL code analysis for secure insights
- Add test results upload with 1-month retention
- Upgrade actions versions (checkout@v4, golangci-lint@v8, upload-artifact@v4)
- Add Go 1.24.x and stable version to test matrix
- Add cache support to improve workflow performance
- Improve Makefile DIR deletion with safe check
- Update README.zh.md language navigation to standard format
- Update dependencies in go.mod and go.sum

16 of 83 new or added lines in 5 files covered. (19.28%)

4 existing lines in 1 file now uncovered.

261 of 711 relevant lines covered (36.71%)

10.11 hits per line

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

75.43
/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 {
80✔
38
        return &CommandConfig{
80✔
39
                DebugMode: NewDebugMode(debugModeOpen), // consistent with the debugModeOpen variable. // 初始值与 debugModeOpen 保持一致。
80✔
40
                MatchPipe: func(outputLine string) bool { return false },
95✔
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 {
5✔
151
        c.TakeExits[exitCode] = "EXPECTED-EXIT-CODES"
5✔
152
        return c
5✔
153
}
5✔
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.WarpResults(done.VAE(command.CombinedOutput()), c.IsShowOutputs(), c.TakeExits)
70✔
165
}
166

167
// ExecWith executes a shell command with the specified name and arguments, using the CommandConfig configuration.
168
// ExecWith 使用 CommandConfig 的配置执行带有指定名称和参数的 shell 命令。
169
func (c *CommandConfig) ExecWith(name string, args []string, runWith func(command *exec.Cmd)) ([]byte, error) {
5✔
170
        const skipDepth = 1
5✔
171

5✔
172
        if err := c.checkConfig(name, args, skipDepth+1); err != nil {
5✔
173
                return nil, erero.Ero(err)
×
174
        }
×
175
        command := c.prepareCommand(name, args)
5✔
176
        runWith(command)
5✔
177
        return utils.WarpResults(done.VAE(command.CombinedOutput()), c.IsShowOutputs(), c.TakeExits)
5✔
178
}
179

180
// IsShowCommand checks if the command should be displayed based on the debug mode.
181
// 检查是否应根据调试模式显示命令。
182
func (c *CommandConfig) IsShowCommand() bool {
80✔
183
        return isShowCommand(c.DebugMode)
80✔
184
}
80✔
185

186
// IsShowOutputs checks if the command results should be displayed based on the debug mode.
187
// 检查是否应根据调试模式显示命令结果。
188
func (c *CommandConfig) IsShowOutputs() bool {
95✔
189
        return isShowOutputs(c.DebugMode)
95✔
190
}
95✔
191

192
// checkConfig validates the command configuration and shows debug info if needed.
193
// checkConfig 验证命令配置并在需要时显示调试信息。
194
func (c *CommandConfig) checkConfig(name string, args []string, skipDepth int) error {
80✔
195
        if name == "" {
80✔
NEW
196
                return erero.New("can-not-execute-with-blank-command-name")
×
197
        }
×
198
        if c.ShellFlag == "" && c.ShellType == "" {
110✔
199
                if strings.Contains(name, " ") {
30✔
200
                        return erero.New("can-not-contains-space-in-command-name")
×
201
                }
×
202
        }
203
        if c.ShellFlag != "" {
130✔
204
                if c.ShellType == "" {
50✔
205
                        return erero.New("can-not-execute-with-wrong-shell-command")
×
206
                }
×
207
        }
208
        if c.ShellType != "" {
130✔
209
                if c.ShellFlag != "-c" {
50✔
210
                        return erero.New("can-not-execute-with-wrong-shell-options")
×
211
                }
×
212
        }
213
        if c.IsShowCommand() {
150✔
214
                debugMessage := c.makeCommandMessage(name, args)
70✔
215
                utils.ShowCommand(debugMessage)
70✔
216
                zaplog.ZAPS.Skip(skipDepth).LOG.Debug("EXEC:", zap.String("CMD", debugMessage))
70✔
217
        }
70✔
218
        return nil
80✔
219
}
220

221
// prepareCommand creates and configures an exec.Cmd based on the CommandConfig settings.
222
// prepareCommand 根据 CommandConfig 设置创建并配置 exec.Cmd。
223
func (c *CommandConfig) prepareCommand(name string, args []string) *exec.Cmd {
80✔
224
        cmd := tern.BFF(c.ShellType != "",
80✔
225
                func() *exec.Cmd {
130✔
226
                        return exec.Command(c.ShellType, c.ShellFlag, name+" "+strings.Join(args, " "))
50✔
227
                },
50✔
228
                func() *exec.Cmd {
30✔
229
                        return exec.Command(name, args...)
30✔
230
                })
30✔
231
        cmd.Dir = c.Path
80✔
232
        // Set environment variables: when c.Envs has no items, Go uses os.Environ()
80✔
233
        // 设置环境变量:当 c.Envs 没有项目时,Go 使用 os.Environ()
80✔
234
        cmd.Env = tern.BF(len(c.Envs) > 0, func() []string {
100✔
235
                return append(os.Environ(), c.Envs...)
20✔
236
        })
20✔
237
        return cmd
80✔
238
}
239

240
// makeCommandMessage constructs a command-line string based on the CommandConfig and given command name and arguments.
241
// makeCommandMessage 根据 CommandConfig 和指定的命令名称及参数构造命令行字符串。
242
func (c *CommandConfig) makeCommandMessage(name string, args []string) string {
70✔
243
        var pts = printgo.NewPTS()
70✔
244
        if c.Path != "" {
90✔
245
                pts.Fprintf("cd %s && ", c.Path)
20✔
246
        }
20✔
247
        if len(c.Envs) > 0 {
90✔
248
                pts.Fprintf("%s ", strings.Join(c.Envs, " "))
20✔
249
        }
20✔
250
        if c.ShellType != "" && c.ShellFlag != "" {
115✔
251
                pts.Fprintf("%s %s '%s'", c.ShellType, c.ShellFlag, escapeSingleQuotes(makeCommandMessage(name, args)))
45✔
252
        } else {
70✔
253
                pts.Fprintf("%s %s", name, strings.Join(args, " "))
25✔
254
        }
25✔
255
        return pts.String()
70✔
256
}
257

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

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

5✔
269
        if err := c.checkConfig(name, args, skipDepth+1); err != nil {
5✔
270
                return nil, erero.Ero(err)
×
271
        }
×
272
        command := c.prepareCommand(name, args)
5✔
273

5✔
274
        stdoutPipe, err := command.StdoutPipe()
5✔
275
        if err != nil {
5✔
276
                return nil, erero.Wro(err)
×
277
        }
×
278

279
        stderrPipe, err := command.StderrPipe()
5✔
280
        if err != nil {
5✔
281
                return nil, erero.Wro(err)
×
282
        }
×
283

284
        stdoutReader := bufio.NewReader(stdoutPipe)
5✔
285
        stderrReader := bufio.NewReader(stderrPipe)
5✔
286
        if err := command.Start(); err != nil {
5✔
287
                return nil, erero.Wro(err)
×
288
        }
×
289

290
        wg := sync.WaitGroup{}
5✔
291
        wg.Add(2)
5✔
292
        var errMatch = false
5✔
293
        var stderrBuffer = printgo.NewPTX()
5✔
294
        go func() {
10✔
295
                defer wg.Done()
5✔
296
                errMatch = c.readPipe(stderrReader, stderrBuffer, "REASON", eroticgo.RED)
5✔
297
        }()
5✔
298
        var outMatch = false
5✔
299
        var stdoutBuffer = printgo.NewPTX()
5✔
300
        go func() {
10✔
301
                defer wg.Done()
5✔
302
                outMatch = c.readPipe(stdoutReader, stdoutBuffer, "OUTPUT", eroticgo.GREEN)
5✔
303
        }()
5✔
304
        wg.Wait()
5✔
305

5✔
306
        // Wait for command to complete and get exit status
5✔
307
        // 等待命令完成并获取退出状态
5✔
308
        erw := command.Wait()
5✔
309

5✔
310
        // When output matched, exit with success status (can succeed even if erw != nil)
5✔
311
        // 当输出匹配成功时,以成功状态退出(即使 erw != nil 也可以成功)
5✔
312
        if outMatch {
5✔
NEW
313
                return utils.WarpResults(done.VAE(stdoutBuffer.Bytes(), erw), c.IsShowOutputs(), c.TakeExits)
×
NEW
314
        }
×
315

316
        // If stderr matched, return with stderr data (e.g., "go: upgraded xxx")
317
        // 如果 stderr 匹配成功,返回 stderr 数据(比如 "go: upgraded xxx")
318
        if errMatch {
5✔
NEW
319
                return utils.WarpResults(done.VAE(stderrBuffer.Bytes(), erw), c.IsShowOutputs(), c.TakeExits)
×
UNCOV
320
        }
×
321

322
        // No match found, check errors in sequence
323
        // 没有匹配,按顺序检查错误
324
        if erw != nil {
5✔
NEW
325
                // Command failed with non-zero exit code
×
NEW
326
                // 命令以非零退出码失败
×
NEW
327
                return utils.WarpResults(done.VAE(stdoutBuffer.Bytes(), erw), c.IsShowOutputs(), c.TakeExits)
×
UNCOV
328
        }
×
329

330
        if stderrBuffer.Len() > 0 {
5✔
NEW
331
                // Command succeeded but has stderr content
×
NEW
332
                // 命令成功但有 stderr 内容
×
UNCOV
333
                return utils.WarpMessage(done.VAE(stdoutBuffer.Bytes(), erero.New(stderrBuffer.String())), c.IsShowOutputs())
×
UNCOV
334
        }
×
335

336
        // Command succeeded with no errors
337
        // 命令成功且无错误
338
        return utils.WarpOutputs(stdoutBuffer.Bytes(), c.IsShowOutputs())
5✔
339
}
340

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

15✔
347
                if c.IsShowOutputs() {
30✔
348
                        zaplog.SUG.Debugln(debugMessage, erotic.Sprint(string(streamLine)))
15✔
349
                }
15✔
350

351
                if (c.MatchMore || !matched) && c.MatchPipe != nil && c.MatchPipe(string(streamLine)) {
15✔
352
                        matched = true
×
353
                }
×
354

355
                if err != nil {
25✔
356
                        if err == io.EOF {
20✔
357
                                ptx.Write(streamLine)
10✔
358
                                return matched
10✔
359
                        }
10✔
NEW
360
                        panic(erero.Wro(err)) // Panic on read error, which is rare // 读取错误时 panic,这种情况很罕见
×
361
                }
362
                ptx.Write(streamLine)
5✔
363
                ptx.Println()
5✔
364
        }
365
}
366

367
// NewConfig creates a shallow clone of the CommandConfig instance.
368
// NewConfig 克隆一个新的 CommandConfig 实例,以便于实现总配置和子配置分隔.
369
func (c *CommandConfig) NewConfig() *CommandConfig {
×
370
        return &CommandConfig{
×
NEW
371
                Envs:      slices.Clone(c.Envs),                          // Clone to avoid data sharing issues // 克隆以避免数据共享问题
×
NEW
372
                Path:      c.Path,                                        // Use same path // 使用相同路径
×
NEW
373
                ShellType: "",                                            // Each command sets its own // 各命令自行设置
×
NEW
374
                ShellFlag: "",                                            // Each command sets its own // 各命令自行设置
×
NEW
375
                DebugMode: c.DebugMode,                                   // Use same debug mode // 使用相同调试模式
×
NEW
376
                MatchPipe: func(outputLine string) bool { return false }, // Each command sets its own // 各命令自行设置
×
377
                MatchMore: false,                                         // Each command sets its own // 各命令自行设置
378
                TakeExits: make(map[int]string),                          // New map as different commands expect different exit codes // 新建映射表,因不同命令期望不同退出码
379
        }
380
}
381

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