arctelix arctelix - 5 months ago 7
Bash Question

Prompt from read -p not displayed when piping stdout/stderr from script

I have a function designed to capture the output from a command and indent each line:

indent_lines () {
local first
local line
local indent=" "

while IFS= read -r line || [[ -n "$line" ]]; do
# remove \r and replace with \r$indent
echo "$indent $(echo "$line" | sed "s/$(printf '\r')/$(printf '\r')$indent /g")"

Which is used like this:

some command 2>&1 | indent_lines

The entire output from "$(some command 2>&1 )" is piped into the indent_lines function and each line of output will be indented. This works except in the case where the subshell invoked calls
read -p
, for example:

get_name () {
echo "this is a line of output 1"
echo "this is a line of output 2"
echo "this is a line of output 3"
read -p "enter your name: " user_input
echo "$user_input is your name"

The output is:

$ get_name 2>&1 | indent_lines
$ this is a line of output 1
$ this is a line of output 2
$ this is a line of output 3

The prompt is not displayed and hangs waiting for input.

Is there any way to get the prompt to display before pausing for input?


The while read loop (like many other tools) on the input side is handling a line at a time. Since the prompt isn't printing a newline at the end, it's thus not handled by the loop.

At a high level, you have two options:

  • Avoid the pipeline for the prompt
  • Add a newline so the content is flushed

Since it's part of the specification that the get_name function can't be modified, what we'll end up doing here is modifying the shell environment to alter how read works.

Avoiding the pipeline

read -p writes its prompt to stderr.

If you want to redirect the prompt, then redirect FD 2.

If you want to ensure that other redirections (such as a 2>&1, which would cause the prompt to go to stdout -- which is being captured) don't apply, then direct to the TTY explicitly:

read -p "enter something" 2>/dev/tty

Adding a newline

Now, if your goal is to run a shell function -- which you can't modify -- with stderr redirected in general but read -p printing prompts directly to the TTY, that can be done, with a hack akin to the following:

reading_to_tty() {
  read() { builtin read "$@" 2>/dev/tty; }
  unset -f read


reading_to_tty get_name 2>&1

...will run get_name, with read commands (and no others) sending stderr content directly to the TTY.

Per extended discussion, another approach to ensure that the prompt is flushed to the pipeline formatting it is to append a newline. The below does that, so the existing pipeline through a formatting function can be used:

reading_with_prompt_newline() { 
  read() {
    if [[ $1 = -p ]]; then 
      set -- "$1" "$2"$'\n' "${@:3}" 
    builtin read "$@" 
  unset -f read 

...used in the same manner as the above:

reading_with_prompt_newline get_name