Search code examples
bashdockertty

Why does docker pseudo-tty mangle output when piped to other command?


Why does pseudo-tty option in docker modify output when docker output is piped to other commands?

Docker uses CRLF line endings when run with -t option. So here's my 2 commands with CR added to other one to make their outputs identical.

❯ docker run --rm -ti bash bash -c "echo -n $'\n\n\n'" | od -c
0000000  \r  \n  \r  \n  \r  \n

❯ docker run --rm bash bash -c "echo -n $'\r\n\r\n\r\n'" | od -c
0000000  \r  \n  \r  \n  \r  \n

Both commands piped to while read -loop (I'd expect both outputs to be identical)

❯ while read -r out; do echo A; done < <(docker run --rm -ti bash bash -c "echo -n $'\n\n\n'")
A
 A
  A

❯ while read -r out; do echo A; done < <(docker run --rm bash bash -c "echo -n $'\r\n\r\n\r\n'")
A
A
A

Why does this happen? Why does pseudo-tty break output? Shouldn't it only tell docker that input is terminal device?

Of course not using -it for non-interactive scripts is a valid solution for this, but doesn't answer 'why'.


Solution

  • It seems that the docker client sets the stdin and the stdout in the raw mode when --tty option is provided. There is call to setRawTerminal(streams) in function setupInput() defined in cli/command/container/hijack.go, which sets both the stdin and the stdout in the raw mode (github link).

    As far as I know, this raw mode then propagates back to the terminal you are using. You can notice this by removing stty -raw from the following example and running them in order.

    In short, raw mode means that the terminal should not do any line processing, i.e. terminal doesn't act on CR (\r).

    Simple demostration without the docker client:

    ❯ while read -r out; do echo A; done < <(bash -c "stty raw; echo -n $'\n\n\n'")   
    A
     A
      A
    
    ❯ while read -r out; do echo A; done < <(bash -c "stty -raw; echo -n $'\n\n\n'")
    A
    A
    A
    

    or just:

    ❯ stty raw; for i in {0..2}; do echo A; done
    A
     A
      A
    
    ❯ stty -raw; for i in {0..2}; do echo A; done
    A
    A
    A