@@ -22,29 +22,6 @@ import (
2222 "github.com/gruntwork-io/terragrunt/internal/util"
2323)
2424
25- type execContextKey struct {}
26-
27- // WithExec returns ctx with the given vexec.Exec installed for use by RunCommandWithOutput.
28- // When set, the executor replaces the default os/exec backend; intended for tests that need to intercept subprocess execution.
29- func WithExec (ctx context.Context , e vexec.Exec ) context.Context {
30- return context .WithValue (ctx , execContextKey {}, e )
31- }
32-
33- func execFromContext (ctx context.Context ) vexec.Exec {
34- e , _ := ctx .Value (execContextKey {}).(vexec.Exec )
35- return e
36- }
37-
38- func envSliceFromMap (env map [string ]string ) []string {
39- out := make ([]string , 0 , len (env ))
40-
41- for k , v := range env {
42- out = append (out , k + "=" + v )
43- }
44-
45- return out
46- }
47-
4825// SignalForwardingDelay is the time to wait before forwarding the signal to the subcommand.
4926//
5027// The signal can be sent to the main process (only `terragrunt`) as well as the process group (`terragrunt` and `terraform`), for example:
@@ -61,6 +38,10 @@ type ShellOptions struct {
6138 EngineConfig * engine.EngineConfig
6239 Telemetry * telemetry.Options
6340 Env map [string ]string
41+ // Exec, when non-nil, replaces the default os/exec backend used by
42+ // RunCommandWithOutput. Intended for tests that need to intercept
43+ // subprocess execution; production code leaves this nil.
44+ Exec vexec.Exec
6445
6546 RootWorkingDir string
6647 WorkingDir string
@@ -176,6 +157,14 @@ func (o *ShellOptions) WithForwardTFStdout(f bool) *ShellOptions {
176157 return o
177158}
178159
160+ // WithExec installs a vexec.Exec backend that replaces the default os/exec
161+ // path used by RunCommandWithOutput. Pass nil to clear it. Intended for tests.
162+ func (o * ShellOptions ) WithExec (e vexec.Exec ) * ShellOptions {
163+ o .Exec = e
164+
165+ return o
166+ }
167+
179168// NoEngine returns true if the user explicitly disabled the engine via --no-engine.
180169// Returns false when EngineOptions is nil (default: don't disable), letting the
181170// other guards (EngineConfig != nil, experiment enabled) decide whether to run.
@@ -272,24 +261,15 @@ func RunCommandWithOutput(
272261 }
273262 }
274263
275- if injected := execFromContext ( ctx ); injected != nil {
276- injectedCmd := injected .Command (ctx , command , args ... )
264+ if runOpts . Exec != nil {
265+ injectedCmd := runOpts . Exec .Command (ctx , command , args ... )
277266 injectedCmd .SetDir (commandDir )
278- injectedCmd .SetEnv (envSliceFromMap (runOpts .Env ))
267+ injectedCmd .SetEnv (util . EnvSliceFromMap (runOpts .Env ))
279268 injectedCmd .SetStdout (cmdStdout )
280269 injectedCmd .SetStderr (cmdStderr )
281270
282271 if err := injectedCmd .Run (); err != nil {
283- return errors .New (util.ProcessExecutionError {
284- Err : err ,
285- Args : args ,
286- Command : command ,
287- Output : output ,
288- WorkingDir : commandDir ,
289- RootWorkingDir : runOpts .RootWorkingDir ,
290- LogShowAbsPaths : runOpts .Writers .LogShowAbsPaths ,
291- DisableSummary : runOpts .Writers .LogDisableErrorSummary ,
292- })
272+ return runOpts .procExecError (err , command , commandDir , args , & output )
293273 }
294274
295275 return nil
@@ -311,39 +291,37 @@ func RunCommandWithOutput(
311291 defer savedConsole .Restore ()
312292
313293 if err := cmd .Start (); err != nil { //nolint:contextcheck // context already passed to exec.Command
314- err = util.ProcessExecutionError {
315- Err : err ,
316- Args : args ,
317- Command : command ,
318- WorkingDir : cmd .Dir ,
319- RootWorkingDir : runOpts .RootWorkingDir ,
320- LogShowAbsPaths : runOpts .Writers .LogShowAbsPaths ,
321- DisableSummary : runOpts .Writers .LogDisableErrorSummary ,
322- }
323-
324- return errors .New (err )
294+ return runOpts .procExecError (err , command , cmd .Dir , args , nil )
325295 }
326296
327297 cancelShutdown := cmd .RegisterGracefullyShutdown (ctx )
328298 defer cancelShutdown ()
329299
330300 if err := cmd .Wait (); err != nil {
331- err = util.ProcessExecutionError {
332- Err : err ,
333- Args : args ,
334- Command : command ,
335- Output : output ,
336- WorkingDir : cmd .Dir ,
337- RootWorkingDir : runOpts .RootWorkingDir ,
338- LogShowAbsPaths : runOpts .Writers .LogShowAbsPaths ,
339- DisableSummary : runOpts .Writers .LogDisableErrorSummary ,
340- }
341-
342- return errors .New (err )
301+ return runOpts .procExecError (err , command , cmd .Dir , args , & output )
343302 }
344303
345304 return nil
346305 })
347306
348307 return & output , err
349308}
309+
310+ // procExecError builds a ProcessExecutionError using the standard fields from ShellOptions.
311+ // Pass nil for output when the failure occurred before any output was captured.
312+ func (o * ShellOptions ) procExecError (err error , command , dir string , args []string , output * util.CmdOutput ) error {
313+ pe := util.ProcessExecutionError {
314+ Err : err ,
315+ Args : args ,
316+ Command : command ,
317+ WorkingDir : dir ,
318+ RootWorkingDir : o .RootWorkingDir ,
319+ LogShowAbsPaths : o .Writers .LogShowAbsPaths ,
320+ DisableSummary : o .Writers .LogDisableErrorSummary ,
321+ }
322+ if output != nil {
323+ pe .Output = * output
324+ }
325+
326+ return errors .New (pe )
327+ }
0 commit comments