Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/shelldoc/cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
1 change: 1 addition & 0 deletions pkg/run/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type Context struct {
FailureStops bool
XMLOutputFile string
MergeStderr bool
NoCleanEnv bool
ReplaceDots bool
DryRun bool
Timeout time.Duration
Expand Down
2 changes: 1 addition & 1 deletion pkg/run/interactions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
21 changes: 20 additions & 1 deletion pkg/shell/shell.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
20 changes: 10 additions & 10 deletions pkg/shell/shell_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,23 @@ 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")
}

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")
}
}

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()
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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()

Expand Down
Loading