Getting a menu to loop when invalid choice is made BASH - 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.

Related

How to pattern match a script argument in Linux bash

Basically, I have to create a bash script that analyzez the arguments of the script and if it's only one, proceeds to pattern match it. If it starts with a number, it says so, if it starts with a letter, it says so and if neither, it says the argument is a string. If you didn't provide an argument, it says you need 1, or if you provided 2 or more. Everything works, apart from the pattern matching itself. I always get the default case. What's wrong with it?
I gotta use case :( .
case $# in
1) case $1 in
/^?[0-9][a-zA-z]*$/) echo "Argument starts with number";;
/^?[a-zA-z][a-zA-z]*$/) echo "Argument starts with letter";;
*) echo "Argument is a string";;
esac;;
0) echo -n "You can't use 0 arguments"
exit 1;;
*) echo -n "You can't use 2 or more arguments"
exit 1;;
esac
exit 0
EDIT: typing this in terminal gives me the default case: l#ubuntu:~/Documents$ ./e4_G_L.sh contor - maybe I wrote the argument wrong and the pattern matching works just fine?
As pointed out in the comments, case only supports globs (also known as wildcards). To check regular expression as in your script you could use [[ … =~ … ]] but that would be overkill. Since you only want to check the first letter, globs are sufficient.
Also, I would rephrase the warning message. Tell the user what to do, not just “You messed up. Good luck guessing on your next try.”.
if [ $# != 1 ]; then
echo "Expected 1 argument but found $#."
exit 1
fi
case "$1" in
[0-9]*) echo "Argument starts with number" ;;
[a-zA-Z]*) echo "Argument starts with letter" ;;
*) echo "Argument is a string";;
esac
Using bash extended patterns:
shopt -s extglob
case $# in
0) echo "You can't use 0 arguments"
exit 1
;;
1) case $1 in
[0-9]+([[:alpha:]]) ) echo "A number followed by letters" ;;
+([[:alpha:]]) ) echo "Only letters" ;;
*) echo "Something else" ;;
esac
;;
*) echo "You can't use 2 or more arguments"
exit 1
;;
esac
Thanks to everybody who bothered to help this lost soul :). It's a task for my Uni so no need for complex hints. The teacher knows what this is about so keeping it short. Ended up using the first answer.

bash: choose default from case when enter is pressed in a "select" prompt

I'm prompting questions in a bash script like this:
optionsAudits=("Yep" "Nope")
echo "Include audits?"
select opt in "${optionsAudits[#]}"; do
case $REPLY in
1) includeAudits=true; break ;;
2) includeAudits=false; break ;;
"\n") echo "You pressed enter"; break ;; # <--- doesn't work
*) echo "What's that?"; exit;;
esac
done
How can I select a default option when enter is pressed? The "\n" case does not catch the enter key.
To complement Aserre's helpful answer, which explains the problem with your code and offers an effective workaround, with background information and a generic, reusable custom select implementation that allows empty input:
Background information
To spell it out explicitly: select itself ignores empty input (just pressing Enter) and simply re-prompts - user code doesn't even get to run in response.
In fact, select uses the empty string to signal to user code that an invalid choice was typed.
That is, if the output variable - $opt, int this case - is empty inside the select statement, the implication is that an invalid choice index was typed by the user.
The output variable receives the chosen option's text - either 'Yep' or 'Nope' in this case - not the index typed by the user.
(By contrast, your code examines $REPLY instead of the output variable, which contains exactly what the user typed, which is the index in case of a valid choice, but may contain extra leading and trailing whitespace).
Note that in the event that you didn't want to allow empty input, you could
simply indicate to the user in the prompt text that ^C (Ctrl+C) can be used to abort the prompt.
Generic custom select function that also accepts empty input
The following function closely emulates what select does while also allowing empty input (just pressing Enter). Note that the function intercepts invalid input, prints a warning, and re-prompts:
# Custom `select` implementation that allows *empty* input.
# Pass the choices as individual arguments.
# Output is the chosen item, or "", if the user just pressed ENTER.
# Example:
# choice=$(selectWithDefault 'one' 'two' 'three')
selectWithDefault() {
local item i=0 numItems=$#
# Print numbered menu items, based on the arguments passed.
for item; do # Short for: for item in "$#"; do
printf '%s\n' "$((++i))) $item"
done >&2 # Print to stderr, as `select` does.
# Prompt the user for the index of the desired item.
while :; do
printf %s "${PS3-#? }" >&2 # Print the prompt string to stderr, as `select` does.
read -r index
# Make sure that the input is either empty or that a valid index was entered.
[[ -z $index ]] && break # empty input
(( index >= 1 && index <= numItems )) 2>/dev/null || { echo "Invalid selection. Please try again." >&2; continue; }
break
done
# Output the selected item, if any.
[[ -n $index ]] && printf %s "${#: index:1}"
}
You could call it as follows:
# Print the prompt message and call the custom select function.
echo "Include audits (default is 'Nope')?"
optionsAudits=('Yep' 'Nope')
opt=$(selectWithDefault "${optionsAudits[#]}")
# Process the selected item.
case $opt in
'Yep') includeAudits=true; ;;
''|'Nope') includeAudits=false; ;; # $opt is '' if the user just pressed ENTER
esac
Alternative implementation that lets the function itself handle the default logic:Thanks, RL-S
This implementation differs from the above in two respects:
It allows you to designate a default among the choices, by prefixing it with !, with the first choice becoming the default otherwise. The default choice is printed with a trailing [default] (and without the leading !). The function then translates empty input into the default choice.
The selected choice is returned as a 1-based index rather than the text. In other words: you can assume that a valid choice was made when the function returns, and that choice is indicated by its position among the choices given.
# Custom `select` implementation with support for a default choice
# that the user can make by pressing just ENTER.
# Pass the choices as individual arguments; e.g. `selectWithDefault Yes No``
# The first choice is the default choice, unless you designate
# one of the choices as the default with a leading '!', e.g.
# `selectWithDefault Yes !No`
# The default choice is printed with a trailing ' [default]'
# Output is the 1-based *index* of the selected choice, as shown
# in the UI.
# Example:
# choice=$(selectWithDefault 'Yes|No|!Abort' )
selectWithDefault() {
local item i=0 numItems=$# defaultIndex=0
# Print numbered menu items, based on the arguments passed.
for item; do # Short for: for item in "$#"; do
[[ "$item" == !* ]] && defaultIndex=$(( $i + 1)) && item="${item:1} [default]"
printf '%s\n' "$((++i))) $item"
done >&2 # Print to stderr, as `select` does.
# Prompt the user for the index of the desired item.
while :; do
printf %s "${PS3-#? }" >&2 # Print the prompt string to stderr, as `select` does.
read -r index
# Make sure that the input is either empty or that a valid index was entered.
[[ -z $index ]] && index=$defaultIndex && break # empty input == default choice
(( index >= 1 && index <= numItems )) 2>/dev/null || { echo "Invalid selection. Please try again." >&2; continue; }
break
done
# Output the selected *index* (1-based).
printf $index
}
Sample call:
# Print the prompt message and call the custom select function,
# designating 'Abort' as the default choice.
echo "Include audits?"
ndx=$(selectWithDefault 'Yes' 'No', '!Abort')
case $ndx in
1) echo "include";;
2) echo "don't include";;
3) echo "abort";;
esac
Optional reading: A more idiomatic version of your original code
Note: This code doesn't solve the problem, but shows more idiomatic use of the select statement; unlike the original code, this code re-displays the prompt if an invalid choice was made:
optionsAudits=("Yep" "Nope")
echo "Include audits (^C to abort)?"
select opt in "${optionsAudits[#]}"; do
# $opt being empty signals invalid input.
[[ -n $opt ]] || { echo "What's that? Please try again." >&2; continue; }
break # a valid choice was made, exit the prompt.
done
case $opt in # $opt now contains the *text* of the chosen option
'Yep')
includeAudits=true
;;
'Nope') # could be just `*` in this case.
includeAudits=false
;;
esac
Note:
The case statement was moved out of the select statement, because the latter now guarantees that only valid inputs can be made.
The case statement tests the output variable ($opt) rather than the raw user input ($REPLY), and that variable contains the choice text, not its index.
Your problem is due to the fact that select will ignore empty input. For your case, read will be more suitable, but you will lose the utility select provides for automated menu creation.
To emulate the behaviour of select, you could do something like that :
#!/bin/bash
optionsAudits=("Yep" "Nope")
while : #infinite loop. be sure to break out of it when a valid choice is made
do
i=1
echo "Include Audits?"
#we recreate manually the menu here
for o in "${optionsAudits[#]}"; do
echo "$i) $o"
let i++
done
read reply
#the user can either type the option number or copy the option text
case $reply in
"1"|"${optionsAudits[0]}") includeAudits=true; break;;
"2"|"${optionsAudits[1]}") includeAudits=false; break;;
"") echo "empty"; break;;
*) echo "Invalid choice. Please choose an existing option number.";;
esac
done
echo "choice : \"$reply\""
Updated answer:
echo "Include audits? 1) Yep, 2) Nope"
read ans
case $ans in
Yep|1 ) echo "yes"; includeAudits=true; v=1 ;;
Nope|2 ) echo "no"; includeAudits=false; v=2 ;;
"" ) echo "default - yes"; includeAudits=true; v=1 ;;
* ) echo "Whats that?"; exit ;;
esac
This accepts either "Yep" or "1" or "enter" to select yes, and accepts "Nope" or "2" for no, and throws away anything else. It also sets v to 1 or 2 depending on whether the user wanted yes or no.
This will do what you are asking for.
options=("option 1" "option 2");
while :
do
echo "Select your option:"
i=1;
for opt in "${options[#]}"; do
echo "$i) $opt";
let i++;
done
read reply
case $reply in
"1"|"${options[0]}"|"")
doSomething1();
break;;
"2"|"${options[1]}")
doSomething2();
break;;
*)
echo "Invalid choice. Please choose 1 or 2";;
esac
done
Assuming that your default option is Yep:
#!/bin/bash
optionsAudits=("Yep" "Nope")
while : #infinite loop. be sure to break out of it when a valid choice is made
do
i=1
echo "Include Audits?: "
#we recreate manually the menu here
for o in "${optionsAudits[#]}"; do
echo " $i) $o"
let i++
done
read -rp "Audit option: " -iYep
#the user can either type the option number or copy the option text
case $REPLY in
"1"|"${optionsAudits[0]}") includeAudits=true; break;;
"2"|"${optionsAudits[1]}") includeAudits=false; break;;
"") includeAudits=true; break;;
*) echo "Invalid choice. Please choose an existing option number.";;
esac
done
echo "choice : \"$REPLY\""
echo "includeAudits : \"$includeAudits\""
Noticed the line:
read -rp "Audit option: " -eiYep
Also I pulled up $reply to $REPLY so that the case decision works better.
The output would now look like this upon hitting ENTER:
Include Audits?:
1) Yep
2) Nope
Audit option: Yep
choice : ""
includeAudits : "true"
#
As an enhancement over select, read -eiYep will supply Yep default value into the input buffer up front.
Only downside of fronting the default value is that one would have to press backspace a few times to enter in their own answer.

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

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

File globbing and matching numbers only

In a bash script I need to verify that the user inputs actual numbers so I have thought the easiest way to make myself sure about that is implementing a case:
case $1 in
[0-9]*)
echo "It's ok"
;;
*)
echo "Ain't good!"
exit 1
;;
esac
But I'm having hard time with file globbing because I can't find a way to demand the $1 value has to be numeric only. Or another way could be excluding all the alternatives:
case $1 in
-*)
echo "Can't be negative"
exit 1
;;
+*)
echo "Must be unsigned"
exit 1
;;
*[a-zA-z]*)
echo "Can't contain letters"
exit 1
;;
esac
The thing is in this case I should be able to block "special" chars like ! ? ^ = ( ) and so forth... I don't know how to acheive it. Please anyone give me a hint?
Actually it would be better to use
*[!0-9]*
instead of
*[^0-9]*
as the first one is POSIX and the second one is a bashism[1].
[1] http://rgeissert.blogspot.com/2013/02/a-bashism-week-negative-matches.html
If you find a non-numeric character anywhere in the string, the input is bad, otherwise it's good:
case "$1" in
*[^0-9]*) echo "first parameter must contain numbers only"; exit 1;;
esac

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