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