1// Copyright 2012 Jonas mg
2//
3// This Source Code Form is subject to the terms of the Mozilla Public
4// License, v. 2.0. If a copy of the MPL was not distributed with this
5// file, You can obtain one at http://mozilla.org/MPL/2.0/.
6
7// Package sh interprets a command line just like it is done in the Bash shell.
8//
9// The main function is Run which lets to call to system commands under a new
10// process. It handles pipes, environment variables, and does pattern expansion.
11package sh
12
13import (
14	"bytes"
15	"errors"
16	"fmt"
17	"io"
18	"os"
19	"os/exec"
20	"path"
21	"path/filepath"
22	"strings"
23)
24
25// Debug shows debug messages in functions like Run.
26var Debug bool
27
28// == Errors
29var (
30	errEnvVar      = errors.New("the format of the variable has to be VAR=value")
31	errNoCmdInPipe = errors.New("no command around of pipe")
32)
33
34type extraCmdError string
35
36func (e extraCmdError) Error() string {
37	return "command not added to " + string(e)
38}
39
40type runError struct {
41	cmd     string
42	debug   string
43	errType string
44	err     error
45}
46
47func (e runError) Error() string {
48	if Debug {
49		if e.debug != "" {
50			e.debug = "\n## DEBUG\n" + e.debug + "\n"
51		}
52		return fmt.Sprintf("Command line: `%s`\n%s\n## %s\n%s", e.cmd, e.debug, e.errType, e.err)
53	}
54	return fmt.Sprintf("\n%s", e.err)
55}
56
57// RunWithMatch executes external commands with access to shell features such as
58// filename wildcards, shell pipes, environment variables, and expansion of the
59// shortcut character "~" to home directory.
60//
61// This function avoids to have execute commands through a shell since an
62// unsanitized input from an untrusted source makes a program vulnerable to
63// shell injection, a serious security flaw which can result in arbitrary
64// command execution.
65//
66// The most of commands return a text in output or an error if any.
67// `match` is used in commands like *grep*, *find*, or *cmp* to indicate if the
68// serach is matched.
69func RunWithMatch(command string) (output []byte, match bool, err error) {
70	var (
71		cmds           []*exec.Cmd
72		outPipes       []io.ReadCloser
73		stdout, stderr bytes.Buffer
74	)
75
76	commands := strings.Split(command, "|")
77	lastIdxCmd := len(commands) - 1
78
79	// Check lonely pipes.
80	for _, cmd := range commands {
81		if strings.TrimSpace(cmd) == "" {
82			err = runError{command, "", "ERR", errNoCmdInPipe}
83			return
84		}
85	}
86
87	for i, cmd := range commands {
88		cmdEnv := _ENV // evironment variables for each command
89		indexArgs := 1 // position where the arguments start
90		fields := strings.Fields(cmd)
91		lastIdxFields := len(fields) - 1
92
93		// == Get environment variables in the first arguments, if any.
94		for j, fCmd := range fields {
95			if fCmd[len(fCmd)-1] == '=' || // VAR= foo
96				(j < lastIdxFields && fields[j+1][0] == '=') { // VAR =foo
97				err = runError{command, "", "ERR", errEnvVar}
98				return
99			}
100
101			if strings.ContainsRune(fields[0], '=') {
102				cmdEnv = append([]string{fields[0]}, _ENV...) // Insert the environment variable
103				fields = fields[1:]                           // and it is removed from arguments
104			} else {
105				break
106			}
107		}
108		// ==
109
110		cmdPath, e := exec.LookPath(fields[0])
111		if e != nil {
112			err = runError{command, "", "ERR", e}
113			return
114		}
115
116		// == Get the path of the next command, if any
117		for j, fCmd := range fields {
118			cmdBase := path.Base(fCmd)
119
120			if cmdBase != "sudo" && cmdBase != "xargs" {
121				break
122			}
123			// It should have an extra command.
124			if j+1 == len(fields) {
125				err = runError{command, "", "ERR", extraCmdError(cmdBase)}
126				return
127			}
128
129			nextCmdPath, e := exec.LookPath(fields[j+1])
130			if e != nil {
131				err = runError{command, "", "ERR", e}
132				return
133			}
134
135			if fields[j+1] != nextCmdPath {
136				fields[j+1] = nextCmdPath
137				indexArgs = j + 2
138			}
139		}
140
141		// == Expansion of arguments
142		expand := make(map[int][]string, len(fields))
143
144		for j := indexArgs; j < len(fields); j++ {
145			// Skip flags
146			if fields[j][0] == '-' {
147				continue
148			}
149
150			// Shortcut character "~"
151			if fields[j] == "~" || strings.HasPrefix(fields[j], "~/") {
152				fields[j] = strings.Replace(fields[j], "~", _HOME, 1)
153			}
154
155			// File name wildcards
156			names, e := filepath.Glob(fields[j])
157			if e != nil {
158				err = runError{command, "", "ERR", e}
159				return
160			}
161			if names != nil {
162				expand[j] = names
163			}
164		}
165
166		// Substitute the names generated for the pattern starting from last field.
167		if len(expand) != 0 {
168			for j := len(fields) - indexArgs; j >= indexArgs; j-- {
169				if v, ok := expand[j]; ok {
170					fields = append(fields[:j], append(v, fields[j+1:]...)...)
171				}
172			}
173		}
174
175		// == Handle arguments with quotes
176		hasQuote := false
177		needUpdate := false
178		tmpFields := []string{}
179
180		for j := indexArgs; j < len(fields); j++ {
181			v := fields[j]
182			lastChar := v[len(v)-1]
183
184			if !hasQuote && (v[0] == '\'' || v[0] == '"') {
185				if !needUpdate {
186					needUpdate = true
187				}
188
189				v = v[1:] // skip quote
190
191				if lastChar == '\'' || lastChar == '"' {
192					v = v[:len(v)-1] // remove quote
193				} else {
194					hasQuote = true
195				}
196
197				tmpFields = append(tmpFields, v)
198				continue
199			}
200
201			if hasQuote {
202				if lastChar == '\'' || lastChar == '"' {
203					v = v[:len(v)-1] // remove quote
204					hasQuote = false
205				}
206				tmpFields[len(tmpFields)-1] += " " + v
207				continue
208			}
209
210			tmpFields = append(tmpFields, v)
211		}
212
213		if needUpdate {
214			fields = append(fields[:indexArgs], tmpFields...)
215		}
216
217		// == Create command
218		c := &exec.Cmd{
219			Path: cmdPath,
220			Args: append([]string{fields[0]}, fields[1:]...),
221			Env:  cmdEnv,
222		}
223
224		// == Connect pipes
225		outPipe, e := c.StdoutPipe()
226		if e != nil {
227			err = runError{command, "", "ERR", e}
228			return
229		}
230
231		if i == 0 {
232			c.Stdin = os.Stdin
233		} else {
234			c.Stdin = outPipes[i-1] // anterior output
235		}
236
237		// == Buffers
238		c.Stderr = &stderr
239
240		// Only save the last output
241		if i == lastIdxCmd {
242			c.Stdout = &stdout
243		}
244
245		// == Start command
246		if e := c.Start(); e != nil {
247			err = runError{command,
248				fmt.Sprintf("- Command: %s\n- Args: %s", c.Path, c.Args),
249				"Start", fmt.Errorf("%s", c.Stderr)}
250			return
251		}
252
253		//
254		cmds = append(cmds, c)
255		outPipes = append(outPipes, outPipe)
256	}
257
258	for _, c := range cmds {
259		if e := c.Wait(); e != nil {
260			_, isExitError := e.(*exec.ExitError)
261
262			// Error type due I/O problems.
263			if !isExitError {
264				err = runError{command,
265					fmt.Sprintf("- Command: %s\n- Args: %s", c.Path, c.Args),
266					"Wait", fmt.Errorf("%s", c.Stderr)}
267				return
268			}
269
270			if c.Stderr != nil {
271				if stderr := fmt.Sprintf("%s", c.Stderr); stderr != "" {
272					stderr = strings.TrimRight(stderr, "\n")
273					err = runError{command,
274						fmt.Sprintf("- Command: %s\n- Args: %s", c.Path, c.Args),
275						"Stderr", fmt.Errorf("%s", stderr)}
276					return
277				}
278			}
279		} else {
280			match = true
281		}
282	}
283
284	Log.Print(command)
285	return stdout.Bytes(), match, nil
286}
287
288// Run executes external commands just like RunWithMatch, but does not return
289// the boolean `match`.
290func Run(command string) (output []byte, err error) {
291	output, _, err = RunWithMatch(command)
292	return
293}
294
295// Runf is like Run, but formats its arguments according to the format.
296// Analogous to Printf().
297func Runf(format string, args ...interface{}) ([]byte, error) {
298	return Run(fmt.Sprintf(format, args...))
299}
300
301// RunWithMatchf is like RunWithMatch, but formats its arguments according to
302// the format. Analogous to Printf().
303func RunWithMatchf(format string, args ...interface{}) ([]byte, bool, error) {
304	return RunWithMatch(fmt.Sprintf(format, args...))
305}
306