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.
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
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:
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"
answer="$(get_menu_answer)"
, you're capturing everything the function prints to stdout -- the menu as well as the answer -- into theanswer
variable rather than letting them go directly to the terminal. – Gordon Davisson Commented Jan 15 at 18:37echo
andprintf
are being captured into youranswer
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>/proc/$$/fd/1
after firstEOF
to fix this. – Cyrus Commented Jan 15 at 18:54