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.
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
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
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