-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathcommand.go
297 lines (262 loc) · 10.7 KB
/
command.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
package osexec
import (
"bufio"
"fmt"
"io"
"os"
"os/exec"
"slices"
"strings"
"sync"
"github.com/yyle88/done"
"github.com/yyle88/erero"
"github.com/yyle88/eroticgo"
"github.com/yyle88/osexec/internal/utils"
"github.com/yyle88/printgo"
"github.com/yyle88/tern"
"github.com/yyle88/zaplog"
"go.uber.org/zap"
)
// CommandConfig represents the configuration for executing shell commands.
// CommandConfig 表示执行 shell 命令的配置。
type CommandConfig struct {
Envs []string // Optional environment variables. // 填写可选的环境变量。
Path string // Optional execution path. // 填写可选的执行路径。
ShellType string // Optional type of shell to use, e.g., bash, zsh. // 填写可选的 shell 类型,例如 bash,zsh。
ShellFlag string // Optional shell flag, e.g., "-c". // 填写可选的 Shell 参数,例如 "-c"。
DebugMode bool // enable debug mode. // 是否启用调试模式,即打印调试的日志。
MatchPipe func(line string) bool
MatchMore bool
}
// NewCommandConfig creates and returns a new CommandConfig instance.
// NewCommandConfig 创建并返回一个新的 CommandConfig 实例。
func NewCommandConfig() *CommandConfig {
return &CommandConfig{
DebugMode: debugModeOpen, // Initial value is consistent with the debugModeOpen variable. // 初始值与 debugModeOpen 变量保持一致。
}
}
// WithEnvs sets the environment variables for CommandConfig and returns the updated instance.
// WithEnvs 设置 CommandConfig 的环境变量并返回更新后的实例。
func (c *CommandConfig) WithEnvs(envs []string) *CommandConfig {
c.Envs = envs
return c
}
// WithPath sets the execution path for CommandConfig and returns the updated instance.
// WithPath 设置 CommandConfig 的执行路径并返回更新后的实例。
func (c *CommandConfig) WithPath(path string) *CommandConfig {
c.Path = path
return c
}
// WithShellType sets the shell type for CommandConfig and returns the updated instance.
// WithShellType 设置 CommandConfig 的 shell 类型并返回更新后的实例。
func (c *CommandConfig) WithShellType(shellType string) *CommandConfig {
c.ShellType = shellType
return c
}
// WithShellFlag sets the shell flag for CommandConfig and returns the updated instance.
// WithShellFlag 设置 CommandConfig 的 shell 参数并返回更新后的实例。
func (c *CommandConfig) WithShellFlag(shellFlag string) *CommandConfig {
c.ShellFlag = shellFlag
return c
}
// WithShell sets both the shell type and shell flag for CommandConfig and returns the updated instance.
// WithShell 同时设置 CommandConfig 的 shell 类型和 shell 参数,并返回更新后的实例。
func (c *CommandConfig) WithShell(shellType, shellFlag string) *CommandConfig {
c.ShellType = shellType
c.ShellFlag = shellFlag
return c
}
// WithBash sets the shell to bash with the "-c" flag and returns the updated instance.
// WithBash 设置 shell 为 bash 并附带 "-c" 参数,返回更新后的实例。
func (c *CommandConfig) WithBash() *CommandConfig {
return c.WithShell("bash", "-c")
}
// WithZsh sets the shell to zsh with the "-c" flag and returns the updated instance.
// WithZsh 设置 shell 为 zsh 并附带 "-c" 参数,返回更新后的实例。
func (c *CommandConfig) WithZsh() *CommandConfig {
return c.WithShell("zsh", "-c")
}
// WithSh sets the shell to sh with the "-c" flag and returns the updated instance.
// WithSh 设置 shell 为 sh 并附带 "-c" 参数,返回更新后的实例。
func (c *CommandConfig) WithSh() *CommandConfig {
return c.WithShell("sh", "-c")
}
// WithDebugMode sets the debug mode for CommandConfig and returns the updated instance.
// WithDebugMode 设置 CommandConfig 的调试模式并返回更新后的实例。
func (c *CommandConfig) WithDebugMode(debugMode bool) *CommandConfig {
c.DebugMode = debugMode
return c
}
// WithDebug sets the debug mode to true for CommandConfig and returns the updated instance.
// WithDebug 将 CommandConfig 的调试模式设置为 true 并返回更新后的实例。
func (c *CommandConfig) WithDebug() *CommandConfig {
return c.WithDebugMode(true)
}
// WithMatchPipe sets the match pipe function for CommandConfig and returns the updated instance.
// WithMatchPipe 设置 CommandConfig 的匹配管道函数并返回更新后的实例。
func (c *CommandConfig) WithMatchPipe(matchPipe func(line string) bool) *CommandConfig {
c.MatchPipe = matchPipe
return c
}
// WithMatchMore sets the match more flag for CommandConfig and returns the updated instance.
// WithMatchMore 设置 CommandConfig 的匹配更多标志并返回更新后的实例。
func (c *CommandConfig) WithMatchMore(matchMore bool) *CommandConfig {
c.MatchMore = matchMore
return c
}
// Exec executes a shell command with the specified name and arguments, using the CommandConfig configuration.
// Exec 使用 CommandConfig 的配置执行带有指定名称和参数的 shell 命令。
func (c *CommandConfig) Exec(name string, args ...string) ([]byte, error) {
if err := c.validateConfig(name, args); err != nil {
return nil, erero.Ero(err)
}
command := c.prepareCommand(name, args)
return utils.WarpMessage(done.VAE(command.CombinedOutput()), c.DebugMode)
}
func (c *CommandConfig) validateConfig(name string, args []string) error {
if name == "" {
return erero.New("can-not-execute-with-empty-command-name")
}
if c.ShellFlag == "" && c.ShellType == "" {
if strings.Contains(name, " ") {
return erero.New("can-not-contains-space-in-command-name")
}
}
if c.ShellFlag != "" {
if c.ShellType == "" {
return erero.New("can-not-execute-with-wrong-shell-command")
}
}
if c.ShellType != "" {
if c.ShellFlag != "-c" {
return erero.New("can-not-execute-with-wrong-shell-options")
}
}
if c.DebugMode {
debugMessage := c.makeCommandMessage(name, args)
utils.ShowCommand(debugMessage)
zaplog.ZAPS.Skip1.LOG.Debug("EXEC:", zap.String("CMD", debugMessage))
}
return nil
}
func (c *CommandConfig) prepareCommand(name string, args []string) *exec.Cmd {
cmd := tern.BFF(c.ShellType != "",
func() *exec.Cmd {
return exec.Command(c.ShellType, c.ShellFlag, name+" "+strings.Join(args, " "))
},
func() *exec.Cmd {
return exec.Command(name, args...)
})
cmd.Dir = c.Path
cmd.Env = tern.BF(len(c.Envs) > 0, func() []string {
return append(os.Environ(), c.Envs...)
})
return cmd
}
// makeCommandMessage constructs a command-line string based on the CommandConfig and given command name and arguments.
// makeCommandMessage 根据 CommandConfig 和指定的命令名称及参数构造命令行字符串。
func (c *CommandConfig) makeCommandMessage(name string, args []string) string {
var pts = printgo.NewPTS()
if c.Path != "" {
pts.WriteString(fmt.Sprintf("cd %s && ", c.Path))
}
if len(c.Envs) > 0 {
pts.WriteString(fmt.Sprintf("%s ", strings.Join(c.Envs, " ")))
}
if c.ShellType != "" && c.ShellFlag != "" {
pts.WriteString(fmt.Sprintf("%s %s '%s'", c.ShellType, c.ShellFlag, escapeSingleQuotes(makeCommandMessage(name, args))))
} else {
pts.WriteString(fmt.Sprintf("%s %s", name, strings.Join(args, " ")))
}
return pts.String()
}
// StreamExec executes a shell command with the specified name and arguments, using the CommandConfig configuration, and returns the output as a byte slice.
// StreamExec 使用 CommandConfig 的配置执行带有指定名称和参数的 shell 命令,并返回输出的字节切片。
func (c *CommandConfig) StreamExec(name string, args ...string) ([]byte, error) {
return c.ExecInPipe(name, args...)
}
// ExecInPipe executes a shell command with the specified name and arguments, using the CommandConfig configuration, and returns the output as a byte slice.
// ExecInPipe 使用 CommandConfig 的配置执行带有指定名称和参数的 shell 命令,并返回输出的字节切片。
func (c *CommandConfig) ExecInPipe(name string, args ...string) ([]byte, error) {
if err := c.validateConfig(name, args); err != nil {
return nil, erero.Ero(err)
}
command := c.prepareCommand(name, args)
stdoutPipe, err := command.StdoutPipe()
if err != nil {
return nil, erero.Wro(err)
}
stderrPipe, err := command.StderrPipe()
if err != nil {
return nil, erero.Wro(err)
}
stdoutReader := bufio.NewReader(stdoutPipe)
stderrReader := bufio.NewReader(stderrPipe)
if err := command.Start(); err != nil {
return nil, erero.Wro(err)
}
wg := sync.WaitGroup{}
wg.Add(2)
var errMatch = false
var stderrBuffer = printgo.NewPTX()
go func() {
defer wg.Done()
errMatch = c.readPipe(stderrReader, stderrBuffer, "REASON", eroticgo.RED)
}()
var outMatch = false
var stdoutBuffer = printgo.NewPTX()
go func() {
defer wg.Done()
outMatch = c.readPipe(stdoutReader, stdoutBuffer, "OUTPUT", eroticgo.GREEN)
}()
wg.Wait()
if outMatch {
return utils.WarpMessage(done.VAE(stdoutBuffer.Bytes(), nil), c.DebugMode)
}
if errMatch { //比如 "go: upgraded github.com/xx/xx vxx => vxx" 这就不算错误,而是正确的
return utils.WarpMessage(done.VAE(stderrBuffer.Bytes(), nil), c.DebugMode)
}
if stderrBuffer.Len() > 0 {
return utils.WarpMessage(done.VAE(stdoutBuffer.Bytes(), erero.New(stderrBuffer.String())), c.DebugMode)
} else {
return utils.WarpMessage(done.VAE(stdoutBuffer.Bytes(), nil), c.DebugMode)
}
}
// readPipe reads from the provided reader and writes to the provided PTX buffer, using the specified debug message and colors.
// readPipe 从提供的 reader 读取数据并写入提供的 PTX 缓冲区,使用指定的调试消息和颜色。
func (c *CommandConfig) readPipe(reader *bufio.Reader, ptx *printgo.PTX, debugMessage string, erotic eroticgo.COLOR) (matched bool) {
for {
streamLine, _, err := reader.ReadLine()
if c.DebugMode {
zaplog.SUG.Debugln(debugMessage, erotic.Sprint(string(streamLine)))
}
if (c.MatchMore || !matched) && c.MatchPipe != nil {
if c.MatchPipe(string(streamLine)) {
matched = true
}
}
if err != nil {
if err == io.EOF {
ptx.Write(streamLine)
return matched
}
panic(erero.Wro(err)) //panic: 读取结果出错很罕见
} else {
ptx.Write(streamLine)
ptx.Println()
}
}
}
// ShallowClone creates a shallow copy of the CommandConfig instance.
// ShallowClone 拷贝个新的 CommandConfig 实例,以便于实现总配置和子配置分隔.
func (c *CommandConfig) ShallowClone() *CommandConfig {
newConfig := new(CommandConfig)
*newConfig = *c
newConfig.Envs = slices.Clone(c.Envs) //这里为了避免踩内存还是得拷贝一份
return newConfig
}
// GetSubClone creates a shallow copy of the CommandConfig instance with a new path and returns the updated instance.
// GetSubClone 创建一个带有新路径的 CommandConfig 实例的浅拷贝并返回更新后的实例。
func (c *CommandConfig) GetSubClone(path string) *CommandConfig {
return c.ShallowClone().WithPath(path)
}