github.com/terraform-modules-krish/terratest@v0.29.0/modules/shell/command.go (about) 1 package shell 2 3 import ( 4 "bufio" 5 "errors" 6 "fmt" 7 "io" 8 "os" 9 "os/exec" 10 "strings" 11 "sync" 12 "syscall" 13 14 "github.com/terraform-modules-krish/terratest/modules/logger" 15 "github.com/terraform-modules-krish/terratest/modules/testing" 16 "github.com/stretchr/testify/require" 17 ) 18 19 // Command is a simpler struct for defining commands than Go's built-in Cmd. 20 type Command struct { 21 Command string // The command to run 22 Args []string // The args to pass to the command 23 WorkingDir string // The working directory 24 Env map[string]string // Additional environment variables to set 25 // Use the specified logger for the command's output. Use logger.Discard to not print the output while executing the command. 26 Logger *logger.Logger 27 } 28 29 // RunCommand runs a shell command and redirects its stdout and stderr to the stdout of the atomic script itself. If 30 // there are any errors, fail the test. 31 func RunCommand(t testing.TestingT, command Command) { 32 err := RunCommandE(t, command) 33 require.NoError(t, err) 34 } 35 36 // RunCommandE runs a shell command and redirects its stdout and stderr to the stdout of the atomic script itself. Any 37 // returned error will be of type ErrWithCmdOutput, containing the output streams and the underlying error. 38 func RunCommandE(t testing.TestingT, command Command) error { 39 output, err := runCommand(t, command) 40 if err != nil { 41 return &ErrWithCmdOutput{err, output} 42 } 43 return nil 44 } 45 46 // RunCommandAndGetOutput runs a shell command and returns its stdout and stderr as a string. The stdout and stderr of 47 // that command will also be logged with Command.Log to make debugging easier. If there are any errors, fail the test. 48 func RunCommandAndGetOutput(t testing.TestingT, command Command) string { 49 out, err := RunCommandAndGetOutputE(t, command) 50 require.NoError(t, err) 51 return out 52 } 53 54 // RunCommandAndGetOutputE runs a shell command and returns its stdout and stderr as a string. The stdout and stderr of 55 // that command will also be logged with Command.Log to make debugging easier. Any returned error will be of type 56 // ErrWithCmdOutput, containing the output streams and the underlying error. 57 func RunCommandAndGetOutputE(t testing.TestingT, command Command) (string, error) { 58 output, err := runCommand(t, command) 59 if err != nil { 60 return output.Combined(), &ErrWithCmdOutput{err, output} 61 } 62 63 return output.Combined(), nil 64 } 65 66 // RunCommandAndGetStdOut runs a shell command and returns solely its stdout (but not stderr) as a string. The stdout and 67 // stderr of that command will also be logged with Command.Log to make debugging easier. If there are any errors, fail 68 // the test. 69 func RunCommandAndGetStdOut(t testing.TestingT, command Command) string { 70 output, err := RunCommandAndGetStdOutE(t, command) 71 require.NoError(t, err) 72 return output 73 } 74 75 // RunCommandAndGetStdOutE runs a shell command and returns solely its stdout (but not stderr) as a string. The stdout 76 // and stderr of that command will also be printed to the stdout and stderr of this Go program to make debugging easier. 77 // Any returned error will be of type ErrWithCmdOutput, containing the output streams and the underlying error. 78 func RunCommandAndGetStdOutE(t testing.TestingT, command Command) (string, error) { 79 output, err := runCommand(t, command) 80 if err != nil { 81 return output.Stdout(), &ErrWithCmdOutput{err, output} 82 } 83 84 return output.Stdout(), nil 85 } 86 87 type ErrWithCmdOutput struct { 88 Underlying error 89 Output *output 90 } 91 92 func (e *ErrWithCmdOutput) Error() string { 93 return fmt.Sprintf("error while running command: %v; %s", e.Underlying, e.Output.Stderr()) 94 } 95 96 // runCommand runs a shell command and stores each line from stdout and stderr in Output. Depending on the logger, the 97 // stdout and stderr of that command will also be printed to the stdout and stderr of this Go program to make debugging 98 // easier. 99 func runCommand(t testing.TestingT, command Command) (*output, error) { 100 command.Logger.Logf(t, "Running command %s with args %s", command.Command, command.Args) 101 102 cmd := exec.Command(command.Command, command.Args...) 103 cmd.Dir = command.WorkingDir 104 cmd.Stdin = os.Stdin 105 cmd.Env = formatEnvVars(command) 106 107 stdout, err := cmd.StdoutPipe() 108 if err != nil { 109 return nil, err 110 } 111 112 stderr, err := cmd.StderrPipe() 113 if err != nil { 114 return nil, err 115 } 116 117 err = cmd.Start() 118 if err != nil { 119 return nil, err 120 } 121 122 output, err := readStdoutAndStderr(t, command.Logger, stdout, stderr) 123 if err != nil { 124 return output, err 125 } 126 127 return output, cmd.Wait() 128 } 129 130 // This function captures stdout and stderr into the given variables while still printing it to the stdout and stderr 131 // of this Go program 132 func readStdoutAndStderr(t testing.TestingT, log *logger.Logger, stdout, stderr io.ReadCloser) (*output, error) { 133 out := newOutput() 134 stdoutReader := bufio.NewReader(stdout) 135 stderrReader := bufio.NewReader(stderr) 136 137 wg := &sync.WaitGroup{} 138 139 wg.Add(2) 140 var stdoutErr, stderrErr error 141 go func() { 142 defer wg.Done() 143 stdoutErr = readData(t, log, stdoutReader, out.stdout) 144 }() 145 go func() { 146 defer wg.Done() 147 stderrErr = readData(t, log, stderrReader, out.stderr) 148 }() 149 wg.Wait() 150 151 if stdoutErr != nil { 152 return out, stdoutErr 153 } 154 if stderrErr != nil { 155 return out, stderrErr 156 } 157 158 return out, nil 159 } 160 161 func readData(t testing.TestingT, log *logger.Logger, reader *bufio.Reader, writer io.StringWriter) error { 162 var line string 163 var readErr error 164 for { 165 line, readErr = reader.ReadString('\n') 166 167 // remove newline, our output is in a slice, 168 // one element per line. 169 line = strings.TrimSuffix(line, "\n") 170 171 // only return early if the line does not have 172 // any contents. We could have a line that does 173 // not not have a newline before io.EOF, we still 174 // need to add it to the output. 175 if len(line) == 0 && readErr == io.EOF { 176 break 177 } 178 179 log.Logf(t, line) 180 if _, err := writer.WriteString(line); err != nil { 181 return err 182 } 183 184 if readErr != nil { 185 break 186 } 187 } 188 if readErr != io.EOF { 189 return readErr 190 } 191 return nil 192 } 193 194 // GetExitCodeForRunCommandError tries to read the exit code for the error object returned from running a shell command. This is a bit tricky to do 195 // in a way that works across platforms. 196 func GetExitCodeForRunCommandError(err error) (int, error) { 197 if errWithOutput, ok := err.(*ErrWithCmdOutput); ok { 198 err = errWithOutput.Underlying 199 } 200 201 // http://stackoverflow.com/a/10385867/483528 202 if exitErr, ok := err.(*exec.ExitError); ok { 203 // The program has exited with an exit code != 0 204 205 // This works on both Unix and Windows. Although package 206 // syscall is generally platform dependent, WaitStatus is 207 // defined for both Unix and Windows and in both cases has 208 // an ExitStatus() method with the same signature. 209 if status, ok := exitErr.Sys().(syscall.WaitStatus); ok { 210 return status.ExitStatus(), nil 211 } 212 return 1, errors.New("could not determine exit code") 213 } 214 215 return 0, nil 216 } 217 218 func formatEnvVars(command Command) []string { 219 env := os.Environ() 220 for key, value := range command.Env { 221 env = append(env, fmt.Sprintf("%s=%s", key, value)) 222 } 223 return env 224 }