Bash: select statement doesn't break - bash

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.

Related

Storing bash script argument with multiple values

I would like to be able to parse an input to a bash shell script that looks like the following.
myscript.sh --casename obstacle1 --output en --variables v P pResidualTT
The best I have so far fails because the last argument has multiple values. The first arguments should only ever have 1 value, but the third could have anything greater than 1. Is there a way to specify that everything after the third argument up to the next set of "--" should be grabbed? I'm going to assume that a user is not constrained to give the arguments in the order that I have shown.
casename=notset
variables=notset
output_format=notset
while [[ $# -gt 1 ]]
do
key="$1"
case $key in
--casename)
casename=$2
shift
;;
--output)
output_format=$2
shift
;;
--variables)
variables="$2"
shift
;;
*)
echo configure option \'$1\' not understood!
echo use ./configure --help to see correct usage!
exit -1
break
;;
esac
shift
done
echo $casename
echo $output_format
echo $variables
One conventional practice (if you're going to do this) is to shift multiple arguments off. That is:
variables=( )
case $key in
--variables)
while (( "$#" >= 2 )) && ! [[ $2 = --* ]]; do
variables+=( "$2" )
shift
done
;;
esac
That said, it's more common to build your calling convention so a caller would pass one -V or --variable argument per following variable -- that is, something like:
myscript --casename obstacle1 --output en -V=v -V=p -V=pResidualTT
...in which case you only need:
case $key in
-V=*|--variable=*) variables+=( "${1#*=}" );;
-V|--variable) variables+=( "$2" ); shift;;
esac

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.

Shell Bash: How to prompt a user to select from a dynamically populated list?

newbie here!
On a Shell Script (bash), after prompting user for name/password the script should read from a server.list and generate option for selection.
How do I provide the user the options from server.list like so:
#Please select from the server list:
1) 10.1.1.xx
2) 10.1.1.xx
3) 10.1.1.xx
Select option [1]:
Any help would be appreciated!
Supposing this source file:
$ cat server.list
10.1.1.xx
10.1.2.xx
10.1.3.xx
Short answer:
select ip in $(cat server.list); do
echo $REPLY $ip
done
Demo
$ select ip in $(cat server.list); do echo $REPLY $ip; done
1) 10.1.1.xx
2) 10.1.2.xx
3) 10.1.3.xx
#? 1
1 10.1.1.xx
#? 2
2 10.1.2.xx
You will have to implement a case loop to do something useful with the ip variable.
Example
select ip in $(cat server.list) exit; do
case $ip in
exit) echo "exiting"
break ;;
*) echo ip $ip;
esac
done
Since an answer already covers how to accomplish this using Bash's select here are two other options.
1. Plain POSIX shell
The following is how you can implement presenting the user with a choice of one option from a list of options in a POSIX shell script without relying on Bash's extensions.
Code
#!/bin/sh
echo 'Please select from the server list:'
nl server.list
count="$(wc -l server.list | cut -f 1 -d' ')"
n=""
while true; do
read -p 'Select option: ' n
# If $n is an integer between one and $count...
if [ "$n" -eq "$n" ] && [ "$n" -gt 0 ] && [ "$n" -le "$count" ]; then
break
fi
done
value="$(sed -n "${n}p" server.list)"
echo "The user selected option number $n: '$value'"
A sample interaction
Please select from the server list:
1 10.1.1.1
2 10.1.1.2
3 10.1.1.3
4 10.1.1.4
5 10.1.1.5
Select option: 0
Select option: -1
Select option: w
list.sh: line 9: [: w: integer expression expected
Select option: 3
The user selected option number 3: '10.1.1.3'
2. Using dialog(1)
If you have dialog(1) installed on the user's machine you can present the user with pseudographical menu.
Code
#!/bin/sh
tempfile="$(mktemp)"
while true; do
dialog --menu 'Please select from the server list' 18 70 15 $(nl server.list) 2>"$tempfile" && break
done
n="$(cat "$tempfile")"
value="$(sed -n "${n}p" server.list)"
rm "$tempfile"
echo "The user selected option number $n: '$value'"
Screenshot

What is the best way to check the getopts status in bash?

I am using following script :
#!/bin/bash
#script to print quality of man
#unnikrishnan 24/Nov/2010
shopt -s -o nounset
declare -rx SCRIPT=${0##*/}
declare -r OPTSTRING="hm:q:"
declare SWITCH
declare MAN
declare QUALITY
if [ $# -eq 0 ];then
printf "%s -h for more information\n" "$SCRIPT"
exit 192
fi
while getopts "$OPTSTRING" SWITCH;do
case "$SWITCH" in
h) printf "%s\n" "Usage $SCRIPT -h -m MAN-NAME -q MAN-QUALITY"
exit 0
;;
m) MAN="$OPTARG"
;;
q) QUALITY="$OPTARG"
;;
\?) printf "%s\n" "Invalid option"
printf "%s\n" "$SWITCH"
exit 192
;;
*) printf "%s\n" "Invalid argument"
exit 192
;;
esac
done
printf "%s is a %s boy\n" "$MAN" "$QUALITY"
exit 0
In this if I am giving the junk option :
./getopts.sh adsas
./getopts.sh: line 32: MAN: unbound variable
you can see it fails. it seems while is not working. What is the best way to solve it.
The getopts builtin returns 1 ("false") when there are no option arguments.
So, your while never executes unless you have option arguments beginning with a -.
Note the last paragraph in the getopts section of bash(1):
getopts returns true if an option, specified or unspecified, is
found. It returns false if the end of options is encountered or
an error occurs.
If you absolutely require MAN, then i suggest you don't make it an option parameter, but a positional parameter. Options are supposed to be optional.
However, if you want to do it as an option, then do:
# initialise MAN to the empty string
MAN=
# loop as rewritten by DigitalRoss
while getopts "$OPTSTRING" SWITCH "$#"; do
case "$SWITCH" in
m) MAN="$OPTARG" ;;
esac
done
# check that you have a value for MAN
[[ -n "$MAN" ]] || { echo "You must supply a MAN's name with -m"; exit 1; }
Even better, print the usage message before exiting - pull it out into a function so you can share it with the -h option's case.
The "best" solution is subjective. One solution would be to give default values to those variables that can be set by options.

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