github.com/influx6/npkg@v0.8.8/nexec/nexec.go (about) 1 package nexec 2 3 import ( 4 "bytes" 5 "context" 6 "errors" 7 "fmt" 8 "io" 9 "os" 10 "os/exec" 11 "runtime" 12 "time" 13 ) 14 15 // nerror ... 16 var ( 17 ErrCommandFailed = errors.New("Command failed to execute succcesfully") 18 ) 19 20 type Log interface { 21 } 22 23 // CommanderOption defines a function type that aguments a commander's field. 24 type CommanderOption func(*Commander) 25 26 // Command sets the command for the Commander. 27 func Command(format string, m ...interface{}) CommanderOption { 28 return func(cm *Commander) { 29 cm.Command = fmt.Sprintf(format, m...) 30 } 31 } 32 33 // SubCommands sets the subcommands for the Commander exec call. 34 // If subcommands are set then the Binary, Flag and Command are ignored 35 // and the values of the subcommand is used. 36 func SubCommands(p ...string) CommanderOption { 37 return func(cm *Commander) { 38 cm.SubCommands = p 39 } 40 } 41 42 // Dir sets the Directory for the Commander exec call. 43 func Dir(p string) CommanderOption { 44 return func(cm *Commander) { 45 cm.Dir = p 46 } 47 } 48 49 // Binary sets the binary command for the Commander. 50 func Binary(bin string, flag string) CommanderOption { 51 return func(cm *Commander) { 52 cm.Binary = bin 53 cm.Flag = flag 54 } 55 } 56 57 // Timeout sets the commander to run in synchronouse mode. 58 func Timeout(d time.Duration) CommanderOption { 59 return func(cm *Commander) { 60 cm.Timeout = d 61 } 62 } 63 64 // Sync sets the commander to run in synchronouse mode. 65 func Sync() CommanderOption { 66 return SetAsync(false) 67 } 68 69 // Async sets the commander to run in asynchronouse mode. 70 func Async() CommanderOption { 71 return SetAsync(true) 72 } 73 74 // SetAsync sets the command for the Commander. 75 func SetAsync(b bool) CommanderOption { 76 return func(cm *Commander) { 77 cm.Async = b 78 } 79 } 80 81 // Input sets the input reader for the Commander. 82 func Input(in io.Reader) CommanderOption { 83 return func(cm *Commander) { 84 cm.In = in 85 } 86 } 87 88 // Output sets the output writer for the Commander. 89 func Output(out io.Writer) CommanderOption { 90 return func(cm *Commander) { 91 cm.Out = out 92 } 93 } 94 95 // Err sets the error writer for the Commander. 96 func Err(err io.Writer) CommanderOption { 97 return func(cm *Commander) { 98 cm.Err = err 99 } 100 } 101 102 // Envs sets the map of environment for the Commander. 103 func Envs(envs map[string]string) CommanderOption { 104 return func(cm *Commander) { 105 cm.Envs = envs 106 } 107 } 108 109 // Apply takes the giving series of CommandOption returning a function that always applies them to passed in commanders. 110 func Apply(ops ...CommanderOption) CommanderOption { 111 return func(cm *Commander) { 112 for _, op := range ops { 113 op(cm) 114 } 115 } 116 } 117 118 // ApplyImmediate applies the options immediately to the Commander. 119 func ApplyImmediate(cm *Commander, ops ...CommanderOption) *Commander { 120 for _, op := range ops { 121 op(cm) 122 } 123 124 return cm 125 } 126 127 // Commander runs provided command within a /bin/sh -c "{COMMAND}", returning 128 // response associatedly. It also attaches if provided stdin, stdout and stderr readers/writers. 129 // Commander allows you to set the binary to use and flag, where each defaults to /bin/sh for binary 130 // and -c for flag respectively. 131 type Commander struct { 132 Async bool 133 Command string 134 SubCommands []string 135 Timeout time.Duration 136 Dir string 137 Binary string 138 Flag string 139 Envs map[string]string 140 In io.Reader 141 Out io.Writer 142 Err io.Writer 143 } 144 145 // New returns a new Commander instance. 146 func New(ops ...CommanderOption) *Commander { 147 cm := new(Commander) 148 149 for _, op := range ops { 150 op(cm) 151 } 152 153 return cm 154 } 155 156 // Exec executes giving command associated within the command with os/exec. 157 func (c *Commander) Exec(ctx context.Context) (int, error) { 158 if c.Binary == "" { 159 if runtime.GOOS != "windows" { 160 c.Binary = "/bin/sh" 161 if c.Flag == "" { 162 c.Flag = "-c" 163 } 164 } 165 166 if runtime.GOOS == "windows" { 167 c.Binary = "cmd" 168 if c.Flag == "" { 169 c.Flag = "/C" 170 } 171 } 172 } 173 174 var cancel func() 175 if c.Timeout > 0 { 176 ctx, cancel = context.WithTimeout(ctx, c.Timeout) 177 } 178 179 if cancel != nil { 180 defer cancel() 181 } 182 183 var execCommand []string 184 185 switch { 186 case c.Command == "" && len(c.SubCommands) != 0: 187 execCommand = c.SubCommands 188 case c.Command != "" && len(c.SubCommands) == 0 && c.Binary != "": 189 execCommand = append(execCommand, c.Binary, c.Flag, c.Command) 190 case c.Command != "" && len(c.SubCommands) != 0 && c.Binary != "": 191 execCommand = append(append(execCommand, c.Binary, c.Flag, c.Command), c.SubCommands...) 192 case c.Command != "" && len(c.SubCommands) == 0 && c.Binary == "": 193 execCommand = append(execCommand, c.Command) 194 case c.Command != "" && len(c.SubCommands) != 0: 195 execCommand = append(append(execCommand, c.Command), c.SubCommands...) 196 default: 197 return -1, errors.New("commands with/without subcommands must be specified") 198 } 199 200 var errCopy bytes.Buffer 201 var multiErr io.Writer 202 203 if c.Err != nil { 204 multiErr = io.MultiWriter(&errCopy, c.Err) 205 } else { 206 multiErr = &errCopy 207 } 208 209 cmder := exec.Command(execCommand[0], execCommand[1:]...) 210 cmder.Dir = c.Dir 211 cmder.Stderr = multiErr 212 cmder.Stdin = c.In 213 cmder.Stdout = c.Out 214 cmder.Env = os.Environ() 215 216 if c.Envs != nil { 217 for name, val := range c.Envs { 218 cmder.Env = append(cmder.Env, fmt.Sprintf("%s=%s", name, val)) 219 } 220 } 221 222 if !c.Async { 223 err := cmder.Run() 224 return getExitStatus(err), err 225 } 226 227 if err := cmder.Start(); err != nil { 228 return getExitStatus(err), err 229 } 230 231 go func() { 232 <-ctx.Done() 233 if cmder.Process == nil { 234 return 235 } 236 237 cmder.Process.Kill() 238 }() 239 240 if err := cmder.Wait(); err != nil { 241 return getExitStatus(err), err 242 } 243 244 if cmder.ProcessState == nil { 245 return 0, nil 246 } 247 248 if !cmder.ProcessState.Success() { 249 return -1, ErrCommandFailed 250 } 251 252 return 0, nil 253 } 254 255 type exitStatus interface { 256 ExitStatus() int 257 } 258 259 func getExitStatus(err error) int { 260 if err == nil { 261 return 0 262 } 263 if e, ok := err.(exitStatus); ok { 264 return e.ExitStatus() 265 } 266 if e, ok := err.(*exec.ExitError); ok { 267 if ex, ok := e.Sys().(exitStatus); ok { 268 return ex.ExitStatus() 269 } 270 } 271 return 1 272 }