linux - Bash function call leads to incorrect (reversed) text output order - Stack Overflow

admin2025-04-25  3

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.

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.

Share Improve this question asked Jan 15 at 18:24 lacinquettelacinquette 655 bronze badges 5
  • 2 When you use answer="$(get_menu_answer)", you're capturing everything the function prints to stdout -- the menu as well as the answer -- into the answer variable rather than letting them go directly to the terminal. – Gordon Davisson Commented Jan 15 at 18:37
  • 1 The echo and printf are being captured into your answer variable here: answer="$(get_menu_answer)" and so you only see those when you finally echo $answer at the end. Similar question here with some good answers: stackoverflow.com/questions/27776665/… – JNevill Commented Jan 15 at 18:38
  • Add >/proc/$$/fd/1 after first EOF to fix this. – Cyrus Commented Jan 15 at 18:54
  • @JNevill Thank you for providing link this question! As I understood there are two main ways of dealing with this problem: calling tty directly and using descriptors (stderr). Tbh I would like to know about other tools for this job. Are there other well known solutions? – lacinquette Commented Jan 15 at 19:07
  • 1 Better to write the menu to stderr than to redirect read's stderr to stdout. Prompts are diagnostic data in the sense the POSIX spec uses when it's setting out guidelines about which stream to use for what: they're telling the user the state of the program (waiting for a specific type of input). – Charles Duffy Commented Jan 15 at 19:24
Add a comment  | 

2 Answers 2

Reset to default 3

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

While others have addressed why OP's current code generates 'reverse' output, I'm going to look at a couple ideas on how to pass a value from the function to the calling/parent process, which in turn should eliminate 'reverse' output worries ...

Assumptions:

  • you wish to have all menus and user interaction displayed in the console
  • the parent process needs to capture just the answer (ie, we don't want to capture the menu and/or user interaction)

There are several ways to allow the parent process to capture the answer ... named pipe, (temp) file, (global) variable, etc. In this case you'll likely find a (global) variable solution the easiest to implement.

If your function 'knows' the global variable name (answer in this case) then you can hardcode it's reference in the function:

$ testfun() { answer=7; }

$ answer=1
$ testfun
$ typeset -p answer
declare -- answer="7"

The (obvious?) downside to this approach is that the calling process and function must be 'in sync' in terms of knowing the variable's name in advance.

A different approach where we provide the function the name of your variable and then the function modifies the variable, via a nameref (bash 4.3+):

$ testfun() { local -n _var="$1"; _var=7; }    # "-n" ==> nameref

$ answer=1 myvar='abc'
$ typeset -p answer myvar
declare -- answer="1"
declare -- myvar="abc"

$ testfun answer                               # pass the *name* (not the value) of the variable to be updated
$ typeset -p answer myvar
declare -- answer="7"
declare -- myvar="abc"

$ testfun myvar                                # pass a different variable *name*
$ typeset -p answer myvar
declare -- answer="7"
declare -- myvar="7"

Rolling these changes into OP's current code ...

For the global variable name approach:

###### replace this:

local answer=""

###### with this:

answer=""

The main call then becomes:

$ unset answer
$ typeset -p answer
bash: typeset: answer: not found

$ get_menu_answer
    a - a option
    b - b option
    c - c option
    q - Quit
Your answer:c
c

$ typeset -p answer
declare -- answer="c"

For the nameref approach we need to change the local call and replace variable names:

###### replace this:

local answer=""

###### with this:

local -n _answer="$1" ; _answer=""

###### and then:

*** change all variable references of 'answer' with '_answer' ***

NOTE: to eliminate an error at run time regarding a circular reference to the answer variable we want to use a nameref variable name (in the function) that we're (hopefully) 100% sure won't be used in the parent environment. In this case I merely preference the variable name with an underscore with the assumption I won't have to worry about a variable named _answer in the parent process

The main call then becomes:

$ unset answer myvar

$ get_menu_answer answer                       # pass variable *name* as parameter
    a - a option
    b - b option
    c - c option
    q - Quit
Your answer:c
c

$ typeset -p answer myvar
declare -- answer="c"
bash: typeset: myvar: not found

$ get_menu_answer myvar                        # pass a different variable *name*
    a - a option
    b - b option
    c - c option
    q - Quit
Your answer:b
b

$ typeset -p answer myvar
declare -- answer="c"
declare -- myvar="b"
转载请注明原文地址:http://anycun.com/QandA/1745563015a90922.html