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

yyle88 / osexec / 15630201076

13 Jun 2025 08:29AM UTC coverage: 39.839% (-2.4%) from 42.202%
15630201076

push

github

yangyile
稍微重构代码

14 of 41 new or added lines in 3 files covered. (34.15%)

22 existing lines in 2 files now uncovered.

247 of 620 relevant lines covered (39.84%)

4.01 hits per line

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

76.53
/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 for executing shell commands.
23
// CommandConfig 表示执行 shell 命令的配置。
24
type CommandConfig struct {
25
        Envs      []string  // Optional environment variables. // 填写可选的环境变量。
26
        Path      string    // Optional execution path. // 填写可选的执行路径。
27
        ShellType string    // Optional type of shell to use, e.g., bash, zsh. // 填写可选的 shell 类型,例如 bash,zsh。
28
        ShellFlag string    // Optional shell flag, e.g., "-c". // 填写可选的 Shell 参数,例如 "-c"。
29
        DebugMode DebugMode // Enable debug mode. // 是否启用调试模式,即打印调试的日志。
30
        MatchPipe func(outputLine string) bool
31
        MatchMore bool
32
        TakeExits map[int]string
33
}
34

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

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

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

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

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

73
// WithShell sets both the shell type and shell flag for CommandConfig and returns the updated instance.
74
// WithShell 同时设置 CommandConfig 的 shell 类型和 shell 参数,并返回更新后的实例。
75
func (c *CommandConfig) WithShell(shellType, shellFlag string) *CommandConfig {
18✔
76
        c.ShellType = shellType
18✔
77
        c.ShellFlag = shellFlag
18✔
78
        return c
18✔
79
}
18✔
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 {
6✔
84
        return c.WithShell("bash", "-c")
6✔
85
}
6✔
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 {
8✔
96
        return c.WithShell("sh", "-c")
8✔
97
}
8✔
98

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

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

112
// WithMatchPipe sets the match pipe function for CommandConfig and returns the updated instance.
113
// WithMatchPipe 设置 CommandConfig 的匹配管道函数并返回更新后的实例。
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 for CommandConfig and returns the updated instance.
120
// WithMatchMore 设置 CommandConfig 的匹配更多标志并返回更新后的实例。
121
func (c *CommandConfig) WithMatchMore(matchMore bool) *CommandConfig {
×
122
        c.MatchMore = matchMore
×
123
        return c
×
124
}
×
125

126
// WithTakeExits sets the accepted exit codes for CommandConfig and returns the updated instance.
127
// WithTakeExits 设置 CommandConfig 的接受退出码集合并返回更新后的实例。
128
func (c *CommandConfig) WithTakeExits(takeExits map[int]string) *CommandConfig {
2✔
129
        //这里需要复制 map 避免出问题,其次是不要使用 clone 以免外面传的是 nil 就不好啦
2✔
130
        expMap := make(map[int]string, len(takeExits))
2✔
131
        for k, v := range takeExits {
4✔
132
                expMap[k] = v
2✔
133
        }
2✔
134
        //这里完全覆盖而不是增补,是因为覆盖更符合预期,否则还得写增补逻辑
135
        c.TakeExits = expMap
2✔
136
        return c
2✔
137
}
138

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

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

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

28✔
158
        if err := c.checkConfig(name, args, skipDepth+1); err != nil {
28✔
159
                return nil, erero.Ero(err)
×
160
        }
×
161
        command := c.prepareCommand(name, args)
28✔
162
        return utils.WarpResults(done.VAE(command.CombinedOutput()), isShowOutputs(c.DebugMode), c.TakeExits)
28✔
163
}
164

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

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

178
func (c *CommandConfig) checkConfig(name string, args []string, skipDepth int) error {
32✔
179
        if name == "" {
32✔
180
                return erero.New("can-not-execute-with-empty-command-name")
×
181
        }
×
182
        if c.ShellFlag == "" && c.ShellType == "" {
44✔
183
                if strings.Contains(name, " ") {
12✔
184
                        return erero.New("can-not-contains-space-in-command-name")
×
185
                }
×
186
        }
187
        if c.ShellFlag != "" {
52✔
188
                if c.ShellType == "" {
20✔
189
                        return erero.New("can-not-execute-with-wrong-shell-command")
×
190
                }
×
191
        }
192
        if c.ShellType != "" {
52✔
193
                if c.ShellFlag != "-c" {
20✔
194
                        return erero.New("can-not-execute-with-wrong-shell-options")
×
195
                }
×
196
        }
197
        if isShowCommand(c.DebugMode) {
60✔
198
                debugMessage := c.makeCommandMessage(name, args)
28✔
199
                utils.ShowCommand(debugMessage)
28✔
200
                zaplog.ZAPS.Skip(skipDepth).LOG.Debug("EXEC:", zap.String("CMD", debugMessage))
28✔
201
        }
28✔
202
        return nil
32✔
203
}
204

205
func (c *CommandConfig) prepareCommand(name string, args []string) *exec.Cmd {
32✔
206
        cmd := tern.BFF(c.ShellType != "",
32✔
207
                func() *exec.Cmd {
52✔
208
                        return exec.Command(c.ShellType, c.ShellFlag, name+" "+strings.Join(args, " "))
20✔
209
                },
20✔
210
                func() *exec.Cmd {
12✔
211
                        return exec.Command(name, args...)
12✔
212
                })
12✔
213
        cmd.Dir = c.Path
32✔
214
        cmd.Env = tern.BF(len(c.Envs) > 0, func() []string {
40✔
215
                return append(os.Environ(), c.Envs...)
8✔
216
        })
8✔
217
        return cmd
32✔
218
}
219

220
// makeCommandMessage constructs a command-line string based on the CommandConfig and given command name and arguments.
221
// makeCommandMessage 根据 CommandConfig 和指定的命令名称及参数构造命令行字符串。
222
func (c *CommandConfig) makeCommandMessage(name string, args []string) string {
28✔
223
        var pts = printgo.NewPTS()
28✔
224
        if c.Path != "" {
36✔
225
                pts.Fprintf("cd %s && ", c.Path)
8✔
226
        }
8✔
227
        if len(c.Envs) > 0 {
36✔
228
                pts.Fprintf("%s ", strings.Join(c.Envs, " "))
8✔
229
        }
8✔
230
        if c.ShellType != "" && c.ShellFlag != "" {
46✔
231
                pts.Fprintf("%s %s '%s'", c.ShellType, c.ShellFlag, escapeSingleQuotes(makeCommandMessage(name, args)))
18✔
232
        } else {
28✔
233
                pts.Fprintf("%s %s", name, strings.Join(args, " "))
10✔
234
        }
10✔
235
        return pts.String()
28✔
236
}
237

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

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

2✔
249
        if err := c.checkConfig(name, args, skipDepth+1); err != nil {
2✔
250
                return nil, erero.Ero(err)
×
251
        }
×
252
        command := c.prepareCommand(name, args)
2✔
253

2✔
254
        stdoutPipe, err := command.StdoutPipe()
2✔
255
        if err != nil {
2✔
256
                return nil, erero.Wro(err)
×
257
        }
×
258

259
        stderrPipe, err := command.StderrPipe()
2✔
260
        if err != nil {
2✔
261
                return nil, erero.Wro(err)
×
262
        }
×
263

264
        stdoutReader := bufio.NewReader(stdoutPipe)
2✔
265
        stderrReader := bufio.NewReader(stderrPipe)
2✔
266
        if err := command.Start(); err != nil {
2✔
267
                return nil, erero.Wro(err)
×
268
        }
×
269

270
        wg := sync.WaitGroup{}
2✔
271
        wg.Add(2)
2✔
272
        var errMatch = false
2✔
273
        var stderrBuffer = printgo.NewPTX()
2✔
274
        go func() {
4✔
275
                defer wg.Done()
2✔
276
                errMatch = c.readPipe(stderrReader, stderrBuffer, "REASON", eroticgo.RED)
2✔
277
        }()
2✔
278
        var outMatch = false
2✔
279
        var stdoutBuffer = printgo.NewPTX()
2✔
280
        go func() {
4✔
281
                defer wg.Done()
2✔
282
                outMatch = c.readPipe(stdoutReader, stdoutBuffer, "OUTPUT", eroticgo.GREEN)
2✔
283
        }()
2✔
284
        wg.Wait()
2✔
285

2✔
286
        if outMatch {
2✔
NEW
287
                return utils.WarpMessage(done.VAE(stdoutBuffer.Bytes(), nil), isShowOutputs(c.DebugMode))
×
288
        }
×
289

290
        if errMatch { //比如 "go: upgraded github.com/xx/xx vxx => vxx" 这就不算错误,而是正确的
2✔
NEW
291
                return utils.WarpMessage(done.VAE(stderrBuffer.Bytes(), nil), isShowOutputs(c.DebugMode))
×
292
        }
×
293

294
        if stderrBuffer.Len() > 0 {
2✔
NEW
295
                return utils.WarpMessage(done.VAE(stdoutBuffer.Bytes(), erero.New(stderrBuffer.String())), isShowOutputs(c.DebugMode))
×
296
        } else {
2✔
297
                return utils.WarpMessage(done.VAE(stdoutBuffer.Bytes(), nil), isShowOutputs(c.DebugMode))
2✔
298
        }
2✔
299
}
300

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

6✔
307
                if isShowOutputs(c.DebugMode) {
12✔
308
                        zaplog.SUG.Debugln(debugMessage, erotic.Sprint(string(streamLine)))
6✔
309
                }
6✔
310

311
                if (c.MatchMore || !matched) && c.MatchPipe != nil && c.MatchPipe(string(streamLine)) {
6✔
312
                        matched = true
×
313
                }
×
314

315
                if err != nil {
10✔
316
                        if err == io.EOF {
8✔
317
                                ptx.Write(streamLine)
4✔
318
                                return matched
4✔
319
                        }
4✔
320
                        panic(erero.Wro(err)) //panic: 读取结果出错很罕见
×
321
                } else {
2✔
322
                        ptx.Write(streamLine)
2✔
323
                        ptx.Println()
2✔
324
                }
2✔
325
        }
326
}
327

328
// NewConfig creates a shallow copy of the CommandConfig instance.
329
// NewConfig 拷贝个新的 CommandConfig 实例,以便于实现总配置和子配置分隔.
NEW
330
func (c *CommandConfig) NewConfig() *CommandConfig {
×
331
        return &CommandConfig{
×
NEW
332
                Envs:      slices.Clone(c.Envs),                          //这里为了避免踩内存还是得拷贝一份
×
NEW
333
                Path:      c.Path,                                        //在相同的位置
×
NEW
334
                ShellType: "",                                            //各个命令会自己设置
×
NEW
335
                ShellFlag: "",                                            //各个命令会自己设置
×
NEW
336
                DebugMode: c.DebugMode,                                   //使用相同的
×
NEW
337
                MatchPipe: func(outputLine string) bool { return false }, //各个命令会自己设置
×
338
                MatchMore: false,                                         //各个命令会自己设置
339
                TakeExits: make(map[int]string),                          //这里很简单因为不同的子命令期望的错误码不同,这里就不克隆这个“有预期的错误码表”,避免错误被忽略
340
        }
341
}
342

343
// SubConfig creates a shallow copy of the CommandConfig instance with a new path and returns the updated instance.
344
// SubConfig 创建一个带有新路径的 CommandConfig 实例的浅拷贝并返回更新后的实例。
NEW
345
func (c *CommandConfig) SubConfig(path string) *CommandConfig {
×
NEW
346
        return c.NewConfig().WithPath(path)
×
UNCOV
347
}
×
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