diff --git a/cmd/shelldoc/cmd/run.go b/cmd/shelldoc/cmd/run.go index 202ff35..568472f 100644 --- a/cmd/shelldoc/cmd/run.go +++ b/cmd/shelldoc/cmd/run.go @@ -29,6 +29,7 @@ func init() { runCmd.Flags().StringVarP(&runContext.ShellName, "shell", "s", "", "The shell to invoke (default: $SHELL)") runCmd.Flags().BoolVarP(&runContext.FailureStops, "fail", "f", false, "Stop on the first failure") runCmd.Flags().BoolVarP(&runContext.MergeStderr, "merge-stderr", "m", false, "Merge stderr into stdout (2>&1) instead of capturing separately") + runCmd.Flags().BoolVar(&runContext.NoCleanEnv, "no-clean-env", false, "Skip TERM=dumb/NO_COLOR=1 overrides (pass the user's environment unmodified)") runCmd.Flags().StringVarP(&runContext.XMLOutputFile, "xml", "x", "", "Write results to the specified output file in JUnitXML format") runCmd.Flags().BoolVarP(&runContext.ReplaceDots, "replace-dots-in-xml-classname", "d", true, "When using filenames as classnames, replace dots with a unicode circle") runCmd.Flags().BoolVarP(&runContext.DryRun, "dry-run", "n", false, "Preview commands without executing them") diff --git a/pkg/run/context.go b/pkg/run/context.go index dc16f28..387bc0a 100644 --- a/pkg/run/context.go +++ b/pkg/run/context.go @@ -21,6 +21,7 @@ type Context struct { FailureStops bool XMLOutputFile string MergeStderr bool + NoCleanEnv bool ReplaceDots bool DryRun bool Timeout time.Duration diff --git a/pkg/run/interactions.go b/pkg/run/interactions.go index 1a2ccaf..ec5a3a3 100644 --- a/pkg/run/interactions.go +++ b/pkg/run/interactions.go @@ -68,7 +68,7 @@ func (runCtx *Context) performInteractions(ctx context.Context, inputfile string return nil, err } // start a background shell, it will run until the function ends - currentShell, err := shell.StartShell(shellpath, runCtx.MergeStderr) + currentShell, err := shell.StartShell(shellpath, runCtx.MergeStderr, runCtx.NoCleanEnv) if err != nil { return nil, fmt.Errorf("unable to start shell: %v", err) } diff --git a/pkg/shell/shell.go b/pkg/shell/shell.go index 4ab797e..87c40dc 100644 --- a/pkg/shell/shell.go +++ b/pkg/shell/shell.go @@ -50,11 +50,30 @@ func DetectShell(selected string) (string, error) { return selected, nil } +// cleanEnv returns the current environment with TERM=dumb and NO_COLOR=1 +// forced, so that child processes do not emit ANSI escape sequences into +// shelldoc's stdout pipe regardless of the user's interactive terminal settings. +func cleanEnv() []string { + env := os.Environ() + result := make([]string, 0, len(env)) + for _, e := range env { + if strings.HasPrefix(e, "TERM=") || strings.HasPrefix(e, "NO_COLOR=") { + continue + } + result = append(result, e) + } + result = append(result, "TERM=dumb", "NO_COLOR=1") + return result +} + // StartShell starts a shell as a background process. // When mergeStderr is true, stderr from each command is redirected into stdout (2>&1). // When false, stderr is captured separately via a temp file and returned alongside stdout. -func StartShell(shell string, mergeStderr bool) (Shell, error) { +func StartShell(shell string, mergeStderr bool, noCleanEnv bool) (Shell, error) { cmd := exec.Command(shell) + if !noCleanEnv { + cmd.Env = cleanEnv() + } stdin, err := cmd.StdinPipe() if err != nil { return Shell{}, fmt.Errorf("Unable to set up input stream for shell %s: %v", shell, err) diff --git a/pkg/shell/shell_test.go b/pkg/shell/shell_test.go index 7e733b0..9ef3e79 100644 --- a/pkg/shell/shell_test.go +++ b/pkg/shell/shell_test.go @@ -22,7 +22,7 @@ func TestMain(m *testing.M) { } func TestShellLifeCycle(t *testing.T) { // The most basic test, start a shell and exit it again - shell, err := StartShell(shellpath, false) + shell, err := StartShell(shellpath, false, false) require.NoError(t, err, "Starting a shell should work") require.NoError(t, shell.Exit(), "Exiting ad running shell should work") } @@ -30,7 +30,7 @@ func TestShellLifeCycle(t *testing.T) { func TestShellLifeCycleRepeated(t *testing.T) { // Can the program start and stop a shell repeatedly? for counter := 0; counter < 16; counter++ { - shell, err := StartShell(shellpath, false) + shell, err := StartShell(shellpath, false, false) require.NoError(t, err, "Starting a shell should work") require.NoError(t, shell.Exit(), "Exiting ad running shell should work") } @@ -38,7 +38,7 @@ func TestShellLifeCycleRepeated(t *testing.T) { func TestReturnCodes(t *testing.T) { // Does the shell report return codes corrrectly? - shell, err := StartShell(shellpath, false) + shell, err := StartShell(shellpath, false, false) require.NoError(t, err, "Starting a shell should work") defer shell.Exit() ctx := context.Background() @@ -58,7 +58,7 @@ func TestReturnCodes(t *testing.T) { func TestCaptureOutput(t *testing.T) { // Does the shell capture and return the lines printed by the command correctly? - shell, err := StartShell(shellpath, false) + shell, err := StartShell(shellpath, false, false) require.NoError(t, err, "Starting a shell should work") defer shell.Exit() ctx := context.Background() @@ -78,7 +78,7 @@ func TestCaptureOutput(t *testing.T) { func TestTimeout(t *testing.T) { // Does the timeout work correctly? - shell, err := StartShell(shellpath, false) + shell, err := StartShell(shellpath, false, false) require.NoError(t, err, "Starting a shell should work") defer shell.Kill() // Use Kill since shell may be in inconsistent state after timeout ctx := context.Background() @@ -92,7 +92,7 @@ func TestTimeout(t *testing.T) { func TestTimeoutExpires(t *testing.T) { // Does timeout trigger correctly for slow commands? - shell, err := StartShell(shellpath, false) + shell, err := StartShell(shellpath, false, false) require.NoError(t, err, "Starting a shell should work") defer shell.Kill() ctx := context.Background() @@ -108,7 +108,7 @@ func TestTimeoutExpires(t *testing.T) { func TestCaptureStderr(t *testing.T) { // Does the shell capture stderr separately from stdout? - shell, err := StartShell(shellpath, false) + shell, err := StartShell(shellpath, false, false) require.NoError(t, err, "Starting a shell should work") defer shell.Exit() ctx := context.Background() @@ -122,7 +122,7 @@ func TestCaptureStderr(t *testing.T) { func TestMergeStderr(t *testing.T) { // Does --merge-stderr combine stderr into stdout? - shell, err := StartShell(shellpath, true) + shell, err := StartShell(shellpath, true, false) require.NoError(t, err, "Starting a shell should work") defer shell.Exit() ctx := context.Background() @@ -137,7 +137,7 @@ func TestMergeStderr(t *testing.T) { func TestStderrDoesNotPollutestdout(t *testing.T) { // Stderr output must not bleed into stdout when captured separately. - shell, err := StartShell(shellpath, false) + shell, err := StartShell(shellpath, false, false) require.NoError(t, err, "Starting a shell should work") defer shell.Exit() ctx := context.Background() @@ -150,7 +150,7 @@ func TestStderrDoesNotPollutestdout(t *testing.T) { func TestContextCancellation(t *testing.T) { // Does context cancellation work correctly? - shell, err := StartShell(shellpath, false) + shell, err := StartShell(shellpath, false, false) require.NoError(t, err, "Starting a shell should work") defer shell.Kill()