Bash - Iterating over select menu options after choosing an option? - bash

I am reading up on Select menus in Bash:
Making menus with the select built-in
I am quite confused when it comes to iterating over the options in the menu and would appreciate any feedback on this.
Using the code here as a starting point:
How can I create a select menu in a shell script?
We get the following output:
root#dev:~/# ./test.sh
1) Option 1
2) Option 2
3) Option 3
4) Quit
Please enter your choice: 1
you chose choice 1
Please enter your choice: 2
you chose choice 2
Please enter your choice: 4
root#dev:~/#
What I would like to try to do, is show the select options again once an option has been chosen, so the output would be like:
root#dev:~/# ./example.sh
1) Option 1
2) Option 2
3) Quit
Please enter your choice: 1
you chose choice 1
1) Option 1
2) Option 2
3) Quit
Please enter your choice: 2
you chose choice 2
1) Option 1
2) Option 2
3) Quit
Please enter your choice: 3
root#dev:~/#
So I have given this a Bash (L) and and tried to loop through the options (array?):
#!/bin/bash
PS3='Please enter your choice: '
options=("Option 1" "Quit")
select opt in "${options[#]}"
do
case $opt in
"Option 1")
echo "you chose choice 1"
# Create an incrementing value
loop=1;
# Loop through each option
for option in ${options[#]} ; do
# Echo the option
echo "$loop) $option";
# Increment the loop
loop=$((loop+1));
done
;;
"Quit")
break
;;
*) echo invalid option;;
esac
done
But then I get an output like:
root#dev:~/# ./stumped.sh
1) Option 1
2) Quit
Please enter your choice: 1
you chose choice 1
1) Option
2) 1
3) Quit
Please enter your choice:
So it seems that the array values are separated by spaces here?
To my understanding: options=("Option 1" "Quit") is creating an array of 2 values and not of 3 however it is being interpreted in Bash as 3 and I am not sure why.
Could someone enlighten me and explain why this is happening?

Let's create a function that shows the menu and echoes the user's choice:
function show_menu {
select option in "${OPTIONS[#]}"; do
echo $option
break
done
}
Now, we can wrap that function in a loop, and only break out if the user picked Quit:
while true; do
option=$(show_menu)
if [[ $option == "Quit" ]]; then
break
fi
done
Voila!

When you want the numbered options shown each iteration, let select do that for you.
options=("Option one" "two" three "and number four" quit)
while true; do
select item in "${options[#]}" ; do
if [ -n "${item}" ]; then
break
fi
echo "Sorry, please enter a number as shown."
break
done;
if [[ "${item}" = "quit" ]]; then
break
fi
if [ -n "${item}" ]; then
echo "Your answer is ${item}"
fi
done

Related

Getting a menu to loop when invalid choice is made BASH

Hey guys so I am trying to get this menu to loop whenever an invalid choice is made in a case statement but having a hard time figuring out what I should be calling back to in my while loop I tried using * as that is whats referenced in case as the invalid choice but it expects an operand when it sees that so I am not sure how to call it back below is the code any help is greatly appreciated.
#Main menu.
#Displays a greeting and waits 8 seconds before clearing the screen
echo "Hello and welcome to the group 97 project we hope you enjoy using our program!"
sleep 8s
clear
while [[ $option -eq "*" ]]
do
#Displays a list of options for the user to choose.
echo "Please select one of the folowing options."
echo -e "\t0. Exit program"
echo -e "\t1. Find the even multiples of any number."
echo -e "\t2. Find the terms of any linear sequence given the rule Un=an+b."
echo -e "\t2. Find the numbers that can be expressed as the product of two nonnegative integers in succession and print them in increasing order."
#Reads the option selection from user and checks it against case for what to do.
read -n 1 option
case $option in
0)
exit ;;
1)
echo task1 ;;
2)
echo task2 ;;
3)
echo task3 ;;
*)
clear
echo "Invalid selection, please try again.";;
esac
done
A select implementation of your menu:
#!/usr/bin/env bash
PS3='Please select one of the options: '
select _ in \
'Exit program' \
'Find the even multiples of any number.' \
'Find the terms of any linear sequence given the rule Un=an+b.' \
'Find the numbers that can be expressed as the product of two nonnegative integers in succession and print them in increasing order.'
do
case $REPLY in
1) exit ;;
2) echo task1 ;;
3) echo task2 ;;
4) echo task3 ;;
*) echo 'Invalid selection, please try again.' ;;
esac
done
Don't reinvent the builtin select
command
choices=(
"Exit program"
"Find the even multiples of any number."
"Find the terms of any linear sequence given the rule Un=an+b."
"Find the numbers that can be expressed as the product of two nonnegative integers in succession and print them in increasing order."
)
PS3="Please select one of the options: "
select choice in "${choices[#]}"; do
case $choice in
"${choices[0]}") exit ;;
"${choices[1]}")
echo task1
break ;;
"${choices[2]}")
echo task2
break ;;
"${choices[3]}")
echo task3
break ;;
esac
done
If you want to stay in the menu until "Exit" then remove the breaks.

How to use goto statement in shell script [duplicate]

This question already has answers here:
Is there a "goto" statement in bash?
(14 answers)
Closed 4 years ago.
I am beginner in shell script. I don't have any idea about how to use goto statement. I am using the following code.
start:
echo "Main Menu"
echo "1 for Copy"
echo "2 for exit"
read NUM
case $NUM in
"1")
echo "CopyNUM"
goto start:;
"2")
echo "Haiiii";
goto start:
*)
echo "ssss";
esac
As others have noted, there's no goto in bash (or other POSIX-like shells) - other, more flexible flow-control constructs take its place.
Look for heading Compound Commands in man bash.
In your case, the select command is the right choice.
Since how to use it may not be obvious, here's something to get you started:
#!/usr/bin/env bash
echo "Main Menu"
# Define the choices to present to the user, which will be
# presented line by line, prefixed by a sequential number
# (E.g., '1) copy', ...)
choices=( 'copy' 'exit' )
# Present the choices.
# The user chooses by entering the *number* before the desired choice.
select choice in "${choices[#]}"; do
# If an invalid number was chosen, $choice will be empty.
# Report an error and prompt again.
[[ -n $choice ]] || { echo "Invalid choice." >&2; continue; }
# Examine the choice.
# Note that it is the choice string itself, not its number
# that is reported in $choice.
case $choice in
copy)
echo "Copying..."
# Set flag here, or call function, ...
;;
exit)
echo "Exiting. "
exit 0
esac
# Getting here means that a valid choice was made,
# so break out of the select statement and continue below,
# if desired.
# Note that without an explicit break (or exit) statement,
# bash will continue to prompt.
break
done
Here is a short example using a select loop to accomplish your goal. You can use a while loop with a custom menu if you want custom formatting, but the basic menu is what select was designed to do:
#!/bin/bash
## array of menu entries
entries=( "for Copy"
"for exit" )
## set prompt for select menu
PS3='Selection: '
while [ "$menu" != 1 ]; do ## outer loop redraws menu each time
printf "\nMain Menu:\n\n" ## heading for menu
select choice in "${entries[#]}"; do ## select displays choices in array
case "$choice" in ## case responds to choice
"for Copy" )
echo "CopyNUM"
break ## break returns control to outer loop
;;
"for exit" )
echo "Haiiii, exiting"
menu=1 ## variable setting exit condition
break
;;
* )
echo "ssss"
break
;;
esac
done
done
exit 0
Use/Output
$ bash select_menu.sh
Main Menu:
1) for Copy
2) for exit
Selection: 1
CopyNUM
Main Menu:
1) for Copy
2) for exit
Selection: 3
ssss
Main Menu:
1) for Copy
2) for exit
Selection: 2
Haiiii, exiting

Bash: select statement doesn't break

Here's a function containing a select statement that doesn't break unless you choose 'quit':
function func_set_branch () {
local _file=$1
local _expr=$2
local _bdate=$3
local _edate=$4
local _mid=$(awk -F'\t' -v ref="${_expr}" 'BEGIN {IGNORECASE = 1} match($0, ref) {print $5}' "$CONF")
if (( $(grep -c . <<<"${_mid}") > 1 )); then
mapfile -t arr <<< "${_mid}"
PS3="Please choose an option "
select option in "${arr[0]}" "${arr[1]}" quit
do
case $option in
1) _mid="${arr[0]}"; break 2;;
2) _mid="${arr[1]}"; break 2;;
quit) exit 0;;
esac
done
fi
sed "s#{{mid}}#${_mid}#
s#{{bdate}}#${_bdate}#
s#{{edate}}#${_edate}#" "$_file"
}
I've tried different levels of break ..no dice. What have I missed after staring at this waaaay tooo long?
Output:
automation#automation-workstation2:~/scripts/branch-fines$ bash get_data.sh -f branch-fines.sql -B coulee -b 2014-01-01 -e 2014-12-31
coulee 2014-01-01 to 2014-12-31
1) 472754
2) 472758
3) quit
Please choose an option 1
Please choose an option 2
Please choose an option 3
automation#automation-workstation2:~/scripts/branch-fines$
UPDATE
Working code with much thanks to rici and glenn jackman.
PS3="Please choose an option "
select option in "${arr[0]}" "${arr[1]}" quit
do
case $option in
"${arr[0]}") MID="${arr[0]}"; break;;
"${arr[1]}") MID="${arr[1]}"; break;;
quit) exit 0;;
esac
done
In the body of the select statement, the specified variable ($option in this case) is set to the value of the selected word, not its index. That's why quit works; you're checking to see if $option is quit, not 3. Similarly, you should be checking for ${arr[0]} and ${arr[1]} rather than 1 and 2.
Since the array values are not 1 or 2, no clause in the case statement will match, so the case statement will do nothing; in that case, no break is executed so the select continues looping.

Bash script, case statement and sub-menus

I wish to run a script with a case statement leading to choices between other lists of choices (sort of sub-menus) between scripts :
#!/bin/bash
echo "Your choice ?"
echo "1) Foo"
echo "2) Bar"
echo "3) Stuff"
read case;
case $case in
#and this is where I would like to allow
#(here a simplified example I do not manage to code)
1) What script ? # may I use another case statement ?
a) Foo1;;
b) Foo2;;
2) What script ? # d°
a) Bar1;;
b) Bar2;;
c) Bar3;;
esac
Foo1, Foo2, Bar1, Bar2 and Bar3 being in fact bash scripts, I would like to call, eg, sh Foo1 from within the script.
How must I proceed :
May I include a case statement within a case statement (better than if statement, if the choices are numerous) ?
And how do I call a script from within another script ?
Thanks in advance for your help
ThG
Dennis' answer should do what you want. Just to illustrate how to do it with functions, so as to make the script more readable:
function which_foo {
local foo_options=("foo1" "foo2" "cancel")
local PS3="Select foo: "
select foo in "${foo_options[#]}"
do
case $REPLY in
1) ./foo1.sh
break ;;
2) ./foo2.sh
break ;;
3) break ;;
esac
done
}
ACTIONS=("foo" "bar" "quit")
PS3="Select action: "
select action in "${ACTIONS[#]}"
do
case $action in
"foo") which_foo
break ;;
"bar") which_bar # Not present in this example
break ;;
"quit") break ;;
esac
done
As an aside, note:
the use of $REPLY in the case inside which_foo, which is automatically set to the number selected by the user, instead of the text of the option.
the use of break in each case options. Without them, select puts you in a loop, so the user would be asked to enter a choice again.
that select never changes the value of PS3 for you, so you have to do it yourself when switching between the different levels of your menu.
Yes, you can nest case statements. You should also consider using a select statement instead of all those echo and read statements.
options=("Option 1" "Option 2" "Option3" "Quit")
optionsprompt='Please enter your choice: '
sub1=("Option 1 sub 1" "Option 1 sub 2")
sub1prompt='Please enter your choice: '
PS3=$optionsprompt
select opt in "${options[#]}"
do
case $opt in
"Option 1")
echo "you chose choice 1"
PS3=$sub1prompt
select sub1opt in "${sub1[#]}"
do
case $sub1opt in
"Option 1 sub 1")
echo "you chose choice 2"
;;
"Option 1 sub 2")
echo "you chose choice 2"
;;
esac
done
;;
"Option 2")
echo "you chose choice 2"
;;
"Option 2")
echo "you chose choice 3"
;;
"Quit")
break
;;
*) echo invalid option;;
esac
done
The first level menu would look like this:
1) Option 1
2) Option 2
3) Option3
4) Quit
Please enter your choice:
However, that can get very hairy very quickly. It would be better to break it up into functions where the inner select/case statements are in separate functions or are abstracted so you can pass arguments to control them.
To call another script, you might want to set a variable that holds the directory. It would look something like this:
basedir=/some/path
case ...
...
$basedir/Foo # call the script

Bash Menu: Return to menu after selection made and executed?

I've got a bash script that writes to a file. at the end of the script I want to display a menu, with line numbers - and have the user be able to select 1 or 2, (etc. up to the number of lines in the file) then have it execute that line.
Up to here is perfect.
However, after the line is executed (say for example it displays another file.) I'd like to return to the menu and let the user select another number. Including zero for exiting the menu.
Once the menu is displayed I have the following. (dumpline being the line of the file read)
dresult=`sed -n "$dumpline"p "$PWD"/"$myday1"_"$myjob".txt`
$dresult
But right now - after running the variable $dresult - it exits the shell (where instead I'd like the menu displayed.
Any thoughts?
thank you in advance.
Here's another way to do a menu which relies on cat having the ability to number the lines of a file (some versions of cat may not have that - see the second example if this is the case). Both examples are for a simple four-item menu:
while [[ 1 ]]
do
cat -n menufile
read -p "Make a selection " choice
case $choice in
1|2)
echo "A or B"
;;
3)
echo "C"
;;
4)
break
;;
*)
echo "Invalid choice"
;;
esac
done
This doesn't require cat -n:
saveIFS="$IFS"
IFS=$'\n'
read -d '' -a menuarray < menufile
IFS="$saveIFS"
for (( i=0; i<${#menuarray[#]}; i++ ))
do
menu=$menu"$(($i+1))) ${menuarray[i]}"$'\n'
done
while [[ 1 ]]
do
echo "$menu"
read -p "Make a selection " choice
case $choice in
1|2)
echo "A or B"
;;
3)
echo "C"
;;
4)
break
;;
*)
echo "Invalid choice"
;;
esac
done
My comments on dz's answer are too long for a comment, so I'm posting them here:
Using seq with select would make a redundant-looking menu, with no correlation between it and the display of the lines in $dumpfile:
ls
echo 'hello'
1) 1
2) 2
etc.
You could do something like this instead:
saveIFS=$IFS
IFS=$'\n'
menu=$(< $dumpfile)
PS3="Make a selection: "
select ACTION in $menu QUIT
do
IFS=$saveIFS
case ...
I think, you need something like a loop. Here is a small skelleton for executing a selected line from file:
#!/bin/bash
dumpfile="bla.txt"
echo "ls
echo 'hello'" > ${dumpfile}
function do_action {
line="$( sed -n "${1},1p" "$dumpfile" )"
(
eval "${line}"
)
}
cat -n $dumpfile
nr=$( cat "${dumpfile}" | wc -l )
PS3="select line nr or nr for QUIT: "
select ACTION in $( seq "$nr" ) QUIT
do
case $ACTION in
QUIT) echo "exit" ; break ;; #EXIT
*) do_action "$ACTION" ;;
esac
done
But be aware of the following:
Using eval might be not allways be a good idea (escaping is hard). Sometimes $line should be sufficient.
Using a subshell prevents changing variables by executing a line of the file. It also does prevent exiting the script from lines that do normally exits a shell.

Resources