Skip to content

Commit 87bbd1b

Browse files
committed
Add package exec
1 parent 9a6501a commit 87bbd1b

File tree

2 files changed

+289
-0
lines changed

2 files changed

+289
-0
lines changed

exec/exec.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* This file is part of arduino-create-agent.
3+
*
4+
* arduino-create-agent is free software; you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation; either version 2 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program; if not, write to the Free Software
16+
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
17+
*
18+
* As a special exception, you may use this file as part of a free software
19+
* library without restriction. Specifically, if other files instantiate
20+
* templates or use macros or inline functions from this file, or you compile
21+
* this file and link it with other files to produce an executable, this
22+
* file does not by itself cause the resulting executable to be covered by
23+
* the GNU General Public License. This exception does not however
24+
* invalidate any other reasons why the executable file might be covered by
25+
* the GNU General Public License.
26+
*
27+
* Copyright 2017 BCMI LABS SA (http://www.arduino.cc/)
28+
*/
29+
// Package exec is just syntactic sugar over os/exec. Allows to execute predefined
30+
// parametric commands in a safe way
31+
//
32+
// Usage:
33+
// cmd := exec.Command{
34+
// Pattern: "echo {interpolate.string}",
35+
// Params: []string{"interpolate.string"},
36+
// }
37+
// opts := map[string]string{"interpolate.string": "hello world"}
38+
// stdout, stderr, err := exec.Local(cmd, opts)
39+
// fmt.Println(err) // nil
40+
// out, _ := ioutil.ReadAll(stdout)
41+
// stdout.Close()
42+
// stderr.Close()
43+
// fmt.Println(string(out)) // hello world
44+
package exec
45+
46+
import (
47+
"io"
48+
"os/exec"
49+
"strings"
50+
51+
shellwords "github.com/mattn/go-shellwords"
52+
"github.com/pkg/errors"
53+
)
54+
55+
// Command is a parametric command that can executed on the machine
56+
type Command struct {
57+
// Pattern is the commandline that will be safely interpolated with the parameters
58+
Pattern string
59+
// Params contains a list of parameters that can be safely interpolated in the pattern
60+
Params []string
61+
}
62+
63+
// Interpolate substitutes the params with the corresponding options
64+
func (cmd Command) Interpolate(opts map[string]string) (interpolated []string, err error) {
65+
pattern := cmd.Pattern
66+
for key, value := range opts {
67+
pattern = strings.Replace(pattern, "{"+key+"}", value, -1)
68+
}
69+
70+
z, err := shellwords.Parse(pattern)
71+
if err != nil {
72+
return nil, err
73+
}
74+
return z, nil
75+
}
76+
77+
// Local executes a command on the local machine, interpolating the command with the
78+
// given options
79+
func Local(c Command, opts map[string]string) (stdout, stderr io.ReadCloser, err error) {
80+
// interpolate
81+
inter, err := c.Interpolate(opts)
82+
if err != nil {
83+
return nil, nil, errors.Wrapf(err, "interpolate")
84+
}
85+
86+
// create command
87+
executable, args := inter[0], inter[1:]
88+
cmd := exec.Command(executable, args...)
89+
90+
stdout, err = cmd.StdoutPipe()
91+
if err != nil {
92+
return nil, nil, errors.Wrapf(err, "retrieve output")
93+
}
94+
95+
stderr, err = cmd.StderrPipe()
96+
if err != nil {
97+
return nil, nil, errors.Wrapf(err, "retrieve output")
98+
}
99+
100+
err = cmd.Start()
101+
if err != nil {
102+
return nil, nil, err
103+
}
104+
105+
return stdout, stderr, nil
106+
}

exec/exec_test.go

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
/*
2+
* This file is part of arduino-create-agent.
3+
*
4+
* arduino-create-agent is free software; you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation; either version 2 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program; if not, write to the Free Software
16+
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
17+
*
18+
* As a special exception, you may use this file as part of a free software
19+
* library without restriction. Specifically, if other files instantiate
20+
* templates or use macros or inline functions from this file, or you compile
21+
* this file and link it with other files to produce an executable, this
22+
* file does not by itself cause the resulting executable to be covered by
23+
* the GNU General Public License. This exception does not however
24+
* invalidate any other reasons why the executable file might be covered by
25+
* the GNU General Public License.
26+
*
27+
* Copyright 2017 BCMI LABS SA (http://www.arduino.cc/)
28+
*/
29+
package exec_test
30+
31+
import (
32+
"errors"
33+
"fmt"
34+
"io/ioutil"
35+
"strings"
36+
"testing"
37+
38+
"github.com/arduino/arduino-create-agent/exec"
39+
)
40+
41+
func Example() {
42+
cmd := exec.Command{
43+
Pattern: "echo {interpolate.string}",
44+
Params: []string{"interpolate.string"},
45+
}
46+
opts := map[string]string{"interpolate.string": "hello world"}
47+
stdout, stderr, err := exec.Local(cmd, opts)
48+
fmt.Println(err) // nil
49+
out, _ := ioutil.ReadAll(stdout)
50+
stdout.Close()
51+
stderr.Close()
52+
fmt.Println(string(out)) // hello world
53+
// Output:
54+
// <nil>
55+
// hello world
56+
}
57+
58+
func TestLocal(t *testing.T) {
59+
cases := []struct {
60+
ID string
61+
Pattern string
62+
Params []string
63+
Options map[string]string
64+
ExpStdout string
65+
ExpStderr string
66+
ExpErr error
67+
}{
68+
{
69+
"command not found",
70+
"foo",
71+
nil,
72+
nil,
73+
"",
74+
"",
75+
errors.New(`exec: "foo": executable file not found in $PATH`),
76+
},
77+
{
78+
"error",
79+
"foo '",
80+
nil,
81+
nil,
82+
"",
83+
"",
84+
errors.New("interpolate: invalid command line string"),
85+
},
86+
{
87+
"hello world",
88+
"echo {interpolate.string}",
89+
[]string{"interpolate.string"},
90+
map[string]string{
91+
"interpolate.string": "hello world",
92+
"ignored": "ignored",
93+
},
94+
"hello world",
95+
"",
96+
nil,
97+
},
98+
{
99+
"inject ; to perform other commands has no effect",
100+
"echo {interpolate.string}",
101+
[]string{"interpolate.string"},
102+
map[string]string{
103+
"interpolate.string": "hello world; ls",
104+
"ignored": "ignored",
105+
},
106+
"hello world",
107+
"",
108+
nil,
109+
},
110+
{
111+
"bash expansion do not work",
112+
"echo {interpolate.string}",
113+
[]string{"interpolate.string"},
114+
map[string]string{
115+
"interpolate.string": "`date`",
116+
"ignored": "ignored",
117+
},
118+
"`date`",
119+
"",
120+
nil,
121+
},
122+
{
123+
"env vars do not work",
124+
"echo {interpolate.string}",
125+
[]string{"interpolate.string"},
126+
map[string]string{
127+
"interpolate.string": "$USER",
128+
"ignored": "ignored",
129+
},
130+
"$USER",
131+
"",
132+
nil,
133+
},
134+
{
135+
"return stderr",
136+
"ls /notexist",
137+
[]string{"interpolate.string"},
138+
map[string]string{
139+
"interpolate.string": "$USER",
140+
"ignored": "ignored",
141+
},
142+
"",
143+
"ls: cannot access '/notexist': No such file or directory",
144+
nil,
145+
},
146+
}
147+
148+
for _, tc := range cases {
149+
t.Run(fmt.Sprintf("%s", tc.ID), func(t *testing.T) {
150+
command := exec.Command{
151+
Pattern: tc.Pattern,
152+
Params: tc.Params,
153+
}
154+
stdout, stderr, e := exec.Local(command, tc.Options)
155+
if !errEq(e, tc.ExpErr) {
156+
t.Errorf("expected e to be '%s', was '%s'", tc.ExpErr, e)
157+
return
158+
}
159+
if e != nil {
160+
return
161+
}
162+
if stdout == nil || stderr == nil {
163+
t.Errorf("stdout and stderr should not be nil")
164+
return
165+
}
166+
out, _ := ioutil.ReadAll(stdout)
167+
stdout.Close()
168+
169+
if strings.TrimSpace(string(out)) != tc.ExpStdout {
170+
t.Errorf("expected stdout to be '%s', was '%s'", tc.ExpStdout, out)
171+
}
172+
err, _ := ioutil.ReadAll(stderr)
173+
stderr.Close()
174+
if strings.TrimSpace(string(err)) != tc.ExpStderr {
175+
t.Errorf("expected stderr to be '%s', was '%s'", tc.ExpStderr, err)
176+
}
177+
})
178+
}
179+
}
180+
181+
func errEq(err1, err2 error) bool {
182+
return fmt.Sprintf("%s", err1) == fmt.Sprintf("%s", err2)
183+
}

0 commit comments

Comments
 (0)