-
Notifications
You must be signed in to change notification settings - Fork 0
/
command.go
123 lines (102 loc) · 2.92 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
package skit
import (
"context"
"fmt"
"os/exec"
"regexp"
"text/template"
"time"
"github.com/pkg/errors"
)
// CommandHandler executes a configured command when input message matches one of
// the regex patterns. cmd and args both will be parsed as golang text templates.
// Named captures from pattern that matches the input message will be used as data
// for rendering command name and every argument.
func CommandHandler(cmd string, args []string, patterns []string) (*Command, error) {
exps, err := ParseExprs(patterns)
if err != nil {
return nil, err
}
cmdTpl, err := template.New("command").Parse(cmd)
if err != nil {
return nil, err
}
argTpls := []template.Template{}
for i, arg := range args {
tpl, err := template.New(fmt.Sprintf("arg%d", i)).Parse(arg)
if err != nil {
return nil, err
}
argTpls = append(argTpls, *tpl)
}
cmdH := &Command{}
cmdH.cmd = *cmdTpl
cmdH.args = argTpls
cmdH.exprs = exps
return cmdH, nil
}
// Command runs configured command when the message matches the regular
// expressions.
type Command struct {
Timeout time.Duration
RedirectErr bool
WorkingDir string
cmd template.Template
args []template.Template
exprs []*regexp.Regexp
}
// Handle executes the command when the message matches the regular expressions.
func (cmd *Command) Handle(ctx context.Context, sk *Skit, ev *MessageEvent) bool {
for _, expr := range cmd.exprs {
match := CaptureAll(expr, ev.Text)
if match == nil {
continue
}
match["event"] = *ev
out, err := cmd.executeCmd(ctx, match)
if err != nil {
msg := fmt.Sprintf("I fucked up :sob: (%v):\n%s", err, string(out))
if err == context.Canceled {
msg = fmt.Sprintf("I was interrupted :face_with_symbols: : \n%s", string(out))
}
sk.SendText(ctx, msg, ev.Channel)
}
sk.SendText(ctx, string(out), ev.Channel)
return true
}
return false
}
func (cmd *Command) executeCmd(ctx context.Context, match map[string]interface{}) ([]byte, error) {
if cmd.Timeout.Seconds() == 0 {
cmd.Timeout = 1 * time.Minute
}
timeoutCtx, cancel := context.WithTimeout(ctx, cmd.Timeout)
defer cancel()
execCmd, err := makeCmd(timeoutCtx, cmd.cmd, cmd.args, match)
if err != nil {
return nil, err
}
execCmd.Dir = cmd.WorkingDir
var out []byte
if cmd.RedirectErr {
out, err = execCmd.CombinedOutput()
} else {
out, err = execCmd.Output()
}
if ctxErr := timeoutCtx.Err(); ctxErr == context.Canceled || ctxErr == context.DeadlineExceeded {
return out, context.Canceled
}
return out, err
}
func makeCmd(ctx context.Context, cmd template.Template, args []template.Template, match map[string]interface{}) (*exec.Cmd, error) {
cmdName, err := Render(cmd, match)
if err != nil {
return nil, errors.Wrap(err, "failed to render command name")
}
cmdArgs, err := RenderAll(args, match)
if err != nil {
return nil, errors.Wrap(err, "failed to render args")
}
execCmd := exec.CommandContext(ctx, cmdName, cmdArgs...)
return execCmd, nil
}