Search code examples
linuxbashshellsh

Bash function call leads to incorrect (reversed) text output order


Trying to implement a simple menu in bash using functions. But for whatever reason bash reverses the output order and first prints prompt to enter answer and then the menu itself.

show_menu () {
    cat <<- EOF
    a - a option
    b - b option
    c - c option
    q - Quit
    EOF
}

get_menu_answer () {
    local answer=""

    while [ -z "$answer" ]; do
        show_menu
        read -rp "Your answer:" answer
        case "$answer" in
            a | b | c | q)
                break
            ;;
            *)
                echo "Unknown option: $answer"
                echo "Try again"
                answer=""
            ;;
        esac
    done
    printf "$answer"
}

answer="$(get_menu_answer)"
echo "$answer"

Sample output:

Your answer:d
Your answer:q
    m - Download manually
    a - Download automatically
    q - Quit
    n - Next
Unknown option: d
Try again
    m - Download manually
    a - Download automatically
    q - Quit
    n - Next
q

As you can see the order is reversed.

However if instead of last 2 lines I just insert get_menu_answer call, then the order is correct (how I want it):

    m - Download manually
    a - Download automatically
    q - Quit
    n - Next
Your answer:d
Unknown option: d
Try again
    m - Download manually
    a - Download automatically
    q - Quit
    n - Next
Your answer:q

Why bash messes up the order of messages and what I can do to solve this problem? Also after a simple debugging I found out that in the first case (the incorrect one) the menu messages are printed after the whole script execution, for example if after echo I add another one echo "Final message" then "Final message" will still be printed before menu options. But I still don't get why this happens.


Solution

  • It's because read -rp will print to stderr while everything else goes to stdout. Since you are using the return from stdout to set answer in the return, you don't see it. If you look at the value of answer afterwords, what you are seeing is the result of all your cats.

    To get this to work, you will need to store the result in a global variable rather than a local one and simply use that. Don't try to use the return.

    To give an example:

    $ cat test.sh 
    #!/bin/bash
    
    function getSomeStuff() {
      echo "some stuff in stdout"
      echo "some more stuff in stdout"
      echo "some stuff in stderr" 1>&2
      read -rp "Your answer:" answer
      echo "$answer"
      return 0
    }
    
    real_answer=$(getSomeStuff)
    echo -e "=============\nAnswer:\n$real_answer"
    

    ... And the result

    $ ./test.sh 
    some stuff in stderr
    Your answer:my answer
    =============
    Answer:
    some stuff in stdout
    some more stuff in stdout
    my answer
    

    Fixed using Globals

    show_menu () {
    cat <<- EOF
      a - a option
      b - b option
      c - c option
      q - Quit
    EOF
    }
    
    get_menu_answer () {
        while [ -z "$answer" ]; do
            show_menu
            read -rp "Your answer:" answer
            case "$answer" in
                a | b | c | q)
                    break
                ;;
                *)
                    echo "Unknown option: $answer"
                    echo "Try again"
                    answer=""
                ;;
            esac
        done
    }
    
    get_menu_answer
    echo "Answer: $answer"
    
    $ ./test.sh 
      a - a option
      b - b option
      c - c option
      q - Quit
    Your answer:a
    Answer: a
    

    Fixed by using stderr to print instead

    You can also use stderr instead.

    $ cat test2.sh 
    show_menu () {
    cat <<- EOF
      a - a option
      b - b option
      c - c option
      q - Quit
    EOF
    }
    
    get_menu_answer () {
        local answer=""
    
        while [ -z "$answer" ]; do
            show_menu
            read -rp "Your answer:" answer
            case "$answer" in
                a | b | c | q)
                    break
                ;;
                *)
                    echo "Unknown option: $answer"
                    echo "Try again"
                    answer=""
                ;;
            esac
        done 1>&2
        printf $answer
    }
    
    answer=$(get_menu_answer)
    echo "Answer: $answer"
    
    $ ./test2.sh 
      a - a option
      b - b option
      c - c option
      q - Quit
    Your answer:a
    Answer: a