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

yyle88 / osexec / 14545565947

19 Apr 2025 04:08AM UTC coverage: 42.105% (+1.5%) from 40.65%
14545565947

push

github

yangyile
修饰逻辑和补充测试函数

2 of 9 new or added lines in 1 file covered. (22.22%)

1 existing line in 1 file now uncovered.

256 of 608 relevant lines covered (42.11%)

4.36 hits per line

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

75.85
/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 bool     // enable debug mode. // 是否启用调试模式,即打印调试的日志。
30
        MatchPipe func(line 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 {
26✔
38
        return &CommandConfig{
26✔
39
                DebugMode: debugModeOpen, // Initial value is consistent with the debugModeOpen variable. // 初始值与 debugModeOpen 变量保持一致。
26✔
40
                MatchPipe: func(line string) bool { return false },
32✔
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 {
4✔
55
        c.Path = path
4✔
56
        return c
4✔
57
}
4✔
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
// WithDebugMode sets the debug mode for CommandConfig and returns the updated instance.
100
// WithDebugMode 设置 CommandConfig 的调试模式并返回更新后的实例。
101
func (c *CommandConfig) WithDebugMode(debugMode bool) *CommandConfig {
12✔
102
        c.DebugMode = debugMode
12✔
103
        return c
12✔
104
}
12✔
105

106
// WithDebug sets the debug mode to true for CommandConfig and returns the updated instance.
107
// WithDebug 将 CommandConfig 的调试模式设置为 true 并返回更新后的实例。
108
func (c *CommandConfig) WithDebug() *CommandConfig {
2✔
109
        return c.WithDebugMode(true)
2✔
110
}
2✔
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(line 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) {
22✔
156
        if err := c.checkConfig(name, args); err != nil {
22✔
157
                return nil, erero.Ero(err)
×
158
        }
×
159
        command := c.prepareCommand(name, args)
22✔
160
        return utils.WarpResults(done.VAE(command.CombinedOutput()), c.DebugMode, c.TakeExits)
22✔
161
}
162

163
// ExecWith executes a shell command with the specified name and arguments, using the CommandConfig configuration.
164
// ExecWith 使用 CommandConfig 的配置执行带有指定名称和参数的 shell 命令。
165
func (c *CommandConfig) ExecWith(name string, args []string, runWith func(command *exec.Cmd)) ([]byte, error) {
2✔
166
        if err := c.checkConfig(name, args); err != nil {
2✔
167
                return nil, erero.Ero(err)
×
168
        }
×
169
        command := c.prepareCommand(name, args)
2✔
170
        runWith(command)
2✔
171
        return utils.WarpResults(done.VAE(command.CombinedOutput()), c.DebugMode, c.TakeExits)
2✔
172
}
173

174
func (c *CommandConfig) checkConfig(name string, args []string) error {
26✔
175
        if name == "" {
26✔
176
                return erero.New("can-not-execute-with-empty-command-name")
×
177
        }
×
178
        if c.ShellFlag == "" && c.ShellType == "" {
32✔
179
                if strings.Contains(name, " ") {
6✔
180
                        return erero.New("can-not-contains-space-in-command-name")
×
181
                }
×
182
        }
183
        if c.ShellFlag != "" {
46✔
184
                if c.ShellType == "" {
20✔
185
                        return erero.New("can-not-execute-with-wrong-shell-command")
×
186
                }
×
187
        }
188
        if c.ShellType != "" {
46✔
189
                if c.ShellFlag != "-c" {
20✔
190
                        return erero.New("can-not-execute-with-wrong-shell-options")
×
191
                }
×
192
        }
193
        if c.DebugMode {
50✔
194
                debugMessage := c.makeCommandMessage(name, args)
24✔
195
                utils.ShowCommand(debugMessage)
24✔
196
                zaplog.ZAPS.Skip1.LOG.Debug("EXEC:", zap.String("CMD", debugMessage))
24✔
197
        }
24✔
198
        return nil
26✔
199
}
200

201
func (c *CommandConfig) prepareCommand(name string, args []string) *exec.Cmd {
26✔
202
        cmd := tern.BFF(c.ShellType != "",
26✔
203
                func() *exec.Cmd {
46✔
204
                        return exec.Command(c.ShellType, c.ShellFlag, name+" "+strings.Join(args, " "))
20✔
205
                },
20✔
206
                func() *exec.Cmd {
6✔
207
                        return exec.Command(name, args...)
6✔
208
                })
6✔
209
        cmd.Dir = c.Path
26✔
210
        cmd.Env = tern.BF(len(c.Envs) > 0, func() []string {
34✔
211
                return append(os.Environ(), c.Envs...)
8✔
212
        })
8✔
213
        return cmd
26✔
214
}
215

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

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

240
// ExecInPipe executes a shell command with the specified name and arguments, using the CommandConfig configuration, and returns the output as a byte slice.
241
// ExecInPipe 使用 CommandConfig 的配置执行带有指定名称和参数的 shell 命令,并返回输出的字节切片。
242
func (c *CommandConfig) ExecInPipe(name string, args ...string) ([]byte, error) {
2✔
243
        if err := c.checkConfig(name, args); err != nil {
2✔
244
                return nil, erero.Ero(err)
×
245
        }
×
246
        command := c.prepareCommand(name, args)
2✔
247

2✔
248
        stdoutPipe, err := command.StdoutPipe()
2✔
249
        if err != nil {
2✔
250
                return nil, erero.Wro(err)
×
251
        }
×
252

253
        stderrPipe, err := command.StderrPipe()
2✔
254
        if err != nil {
2✔
255
                return nil, erero.Wro(err)
×
256
        }
×
257

258
        stdoutReader := bufio.NewReader(stdoutPipe)
2✔
259
        stderrReader := bufio.NewReader(stderrPipe)
2✔
260
        if err := command.Start(); err != nil {
2✔
261
                return nil, erero.Wro(err)
×
262
        }
×
263

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

2✔
280
        if outMatch {
2✔
281
                return utils.WarpMessage(done.VAE(stdoutBuffer.Bytes(), nil), c.DebugMode)
×
282
        }
×
283

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

288
        if stderrBuffer.Len() > 0 {
2✔
289
                return utils.WarpMessage(done.VAE(stdoutBuffer.Bytes(), erero.New(stderrBuffer.String())), c.DebugMode)
×
290
        } else {
2✔
291
                return utils.WarpMessage(done.VAE(stdoutBuffer.Bytes(), nil), c.DebugMode)
2✔
292
        }
2✔
293
}
294

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

6✔
301
                if c.DebugMode {
12✔
302
                        zaplog.SUG.Debugln(debugMessage, erotic.Sprint(string(streamLine)))
6✔
303
                }
6✔
304

305
                if (c.MatchMore || !matched) && c.MatchPipe != nil && c.MatchPipe(string(streamLine)) {
6✔
NEW
306
                        matched = true
×
UNCOV
307
                }
×
308

309
                if err != nil {
10✔
310
                        if err == io.EOF {
8✔
311
                                ptx.Write(streamLine)
4✔
312
                                return matched
4✔
313
                        }
4✔
314
                        panic(erero.Wro(err)) //panic: 读取结果出错很罕见
×
315
                } else {
2✔
316
                        ptx.Write(streamLine)
2✔
317
                        ptx.Println()
2✔
318
                }
2✔
319
        }
320
}
321

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

337
// GetSubClone creates a shallow copy of the CommandConfig instance with a new path and returns the updated instance.
338
// GetSubClone 创建一个带有新路径的 CommandConfig 实例的浅拷贝并返回更新后的实例。
339
func (c *CommandConfig) GetSubClone(path string) *CommandConfig {
×
340
        return c.ShallowClone().WithPath(path)
×
341
}
×
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