-
Notifications
You must be signed in to change notification settings - Fork 1
/
main.go
156 lines (143 loc) · 3.39 KB
/
main.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
package main
import (
"bufio"
"errors"
"flag"
"fmt"
"io"
"log"
"os"
"os/exec"
"regexp"
"strings"
)
const (
// CommentRx matches comment lines.
CommentRx = `^[\s]*#`
// NameRx is much tighter than Posix, which accepts anything but NUL and '=',
// but laxer than shells, which do not accept dots. Names are assumed to be pre-trimmed.
NameRx = `^[[:alpha:]][-._a-zA-Z0-9]*`
)
var (
commentRx = regexp.MustCompile(CommentRx)
nameRx = regexp.MustCompile(NameRx)
)
type env map[string]string
func envFromEnv() env {
e := make(env)
for _, row := range os.Environ() {
// Pairs in the environment are assumed to be valid.
pair := strings.SplitN(row, "=", 2)
k, v := pair[0], pair[1]
e[k] = v
}
return e
}
func envFromReader(r io.Reader) env {
e := make(env)
scanner := bufio.NewScanner(r)
for scanner.Scan() {
row := scanner.Text()
if commentRx.MatchString(row) {
continue
}
parts := strings.SplitN(row, "=", 2)
if len(parts) != 2 {
continue
}
k, v := parts[0], parts[1]
k = strings.Trim(k, " \t")
v = strings.Trim(v, " \t")
if !nameRx.MatchString(k) {
log.Printf(`rejected variable: "%s"`, k)
continue
}
e[k] = v
}
return e
}
// Merge combines two env maps. If keys overlap, the newer one in the argument
// map overwrites the value found in the receiver map, as in PHP array_merge.
func (e env) Merge(f env) env {
res := make(env, len(e)+len(f))
for k, v := range e {
res[k] = v
}
for k, v := range f {
res[k] = v
}
return res
}
func readCloser(args []string) (io.ReadCloser, *flag.FlagSet, error) {
if len(args) < 2 {
return nil, nil, errors.New("need at least a command to run")
}
fs := flag.NewFlagSet(args[0], flag.ContinueOnError)
inName := fs.String("f", ".env", "The file from which to read the environment variables")
if err := fs.Parse(args[1:]); err != nil {
return nil, nil, fmt.Errorf("failed parsing flags: %w", err)
}
if len(fs.Args()) == 0 {
return nil, nil, errors.New("no command to run")
}
inFile, err := os.Open(*inName)
if err != nil {
return nil, fs, fmt.Errorf("failed reading %s: %v", *inName, err)
}
return inFile, fs, nil
}
func run(env env, name string, args []string) error {
fEnv := make([]string, 0, len(env))
for k, v := range env {
fEnv = append(fEnv, fmt.Sprintf("%s=%s", k, v))
}
cmd := exec.Command(name, args...)
cmd.Env = fEnv
stdout, err := cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("failed acquiring %s standard output: %v", name, err)
}
stderr, err := cmd.StderrPipe()
if err != nil {
return fmt.Errorf("failed acquiring %s standard error: %v", name, err)
}
err = cmd.Start()
if err != nil {
return fmt.Errorf("failed starting %s: %v", name, err)
}
go io.Copy(os.Stdout, stdout)
go io.Copy(os.Stderr, stderr)
return cmd.Wait()
}
func main() {
var (
err error
exitCode int
)
rc, fs, err := readCloser(os.Args)
if err != nil {
log.Fatal(err)
}
defer func() {
errClose := rc.Close()
if err != nil || errClose != nil {
os.Exit(exitCode)
}
}()
env := envFromReader(rc)
env = env.Merge(envFromEnv())
toRun := fs.Args()
// Length was checked during readCloser().
name := toRun[0]
if err := run(env, name, toRun[1:]); err == nil {
return
}
var exit *exec.ExitError
ok := errors.As(err, &exit)
if !ok {
log.Printf("non-exit error running %s: %v", name, err)
exitCode = 1
}
log.Printf("exit error running %s: %v", name, err)
exitCode = exit.ExitCode()
}