bash interactive menu, exit on empty input - bash

I have tried to piece together a script to list different jws directories and allow user to select different client directories before it continues on to edit jnlp files within the directory. I have the edit part working, I have the menu working mostly; I can't figure out how to exit the loop once the selections have been made.
I'd like it to exit once ENTER is hit without a number selection, and continue with the next part of the script.
function update_jnlp
{
while :
do
# JNLP update submenu
options=($(ls /tmp/test/ | grep "jws$"))
menu() {
clear
echo "Locally installed jnlps:"
for i in ${!options[#]}; do
printf "%3d%s) %s\n" $((i+1)) "${choices[i]:- }" "${options[i]}"
done
[[ "$msg" ]] && echo "$msg"; :
}
prompt="Check an option (again to uncheck, ENTER when done): "
while menu && read -rp "$prompt" num && [[ "$num" ]]; do
[[ "$num" != *[![:digit:]]* ]] &&
(( num > 0 && num <= ${#options[#]} )) ||
{ msg="Invalid option: $num"; continue; }
((num--)); msg="${options[num]} was ${choices[num]:+un}checked"
[[ "${choices[num]}" ]] && choices[num]="" || choices[num]="+"
done
printf "You selected"; msg=" nothing"
for i in ${!options[#]}; do
[[ "${choices[i]}" ]] && { printf " %s" "${options[i]}"; msg=""; }
done
done
for i in ${choices[#]}; do
printf "%s\n" ${choices[#]};
echo "fun"
done
echo "#msg"
# here is the script to edit the files now contained as ${choices[#]}
}
I recognize that I am in a menu loop and that I need to validate that input from the read command = "" or that $prompt input is null, and I assume I break from there

Please format your code more readably. As it stands, the erratic indentation makes it close to unreadable. Well, the indentation combined with defining a function in the body of a loop inside another function, and some of the more inscrutable loop bodies I've ever seen. (I thought I'd seen quite a lot; clearly, I've still got some learning to do.)
With the code formatted somewhat more orthodoxly, you have:
function update_jnlp
{
while :
do
# JNLP update submenu
options=($(ls /tmp/test/ | grep "jws$"))
menu() {
clear
echo "Locally installed jnlps:"
for i in ${!options[#]}; do
printf "%3d%s) %s\n" $((i+1)) "${choices[i]:- }" "${options[i]}"
done
[[ "$msg" ]] && echo "$msg"; :
}
prompt="Check an option (again to uncheck, ENTER when done): "
while menu && read -rp "$prompt" num && [[ "$num" ]]; do
[[ "$num" != *[![:digit:]]* ]] &&
(( num > 0 && num <= ${#options[#]} )) ||
{ msg="Invalid option: $num"; continue; }
((num--)); msg="${options[num]} was ${choices[num]:+un}checked"
[[ "${choices[num]}" ]] && choices[num]="" || choices[num]="+"
done
printf "You selected"; msg=" nothing"
for i in ${!options[#]}; do
[[ "${choices[i]}" ]] && { printf " %s" "${options[i]}"; msg=""; }
done
echo "$msg" # Added
break # Added
done
for i in ${choices[#]}; do
printf "%s\n" ${choices[#]};
echo "fun"
done
echo "#msg"
# here is the script to edit the files now contained as ${choices[#]}
}
When I ran it, I selected 1, 3, 7 and then hit return, and the final page of output looked like:
Locally installed jnlps:
1+) abc.jws
2 ) def.jws
3+) ghi.jws
4 ) jkl.jws
5 ) mno.jws
6 ) pqr.jws
7+) stu.jws
8 ) vwx.jws
9 ) xyz.jws
stu.jws was checked
Check an option (again to uncheck, ENTER when done):
You selected abc.jws ghi.jws stu.jws
+
+
+
fun
+
+
+
fun
+
+
+
fun
#msg
Without the added break, you don't get to see what's selected because the outer while : loop clears the screen too quickly. With the echo "$msg" added, you get You selected nothing as you'd want.
The material after the main while : loop is clearly not finalized.
In fact, you don't need the while : loop at all, or the incomplete material. You could use:
function update_jnlp
{
# JNLP update submenu
options=($(ls /tmp/test/ | grep "jws$"))
menu() {
clear
echo "Locally installed jnlps:"
for i in ${!options[#]}; do
printf "%3d%s) %s\n" $((i+1)) "${choices[i]:- }" "${options[i]}"
done
[[ "$msg" ]] && echo "$msg"; :
}
prompt="Check an option (again to uncheck, ENTER when done): "
while menu && read -rp "$prompt" num && [[ -n "$num" ]]; do
[[ "$num" != *[![:digit:]]* ]] &&
(( num > 0 && num <= ${#options[#]} )) ||
{ msg="Invalid option: $num"; continue; }
((num--)); msg="${options[num]} was ${choices[num]:+un}checked"
[[ "${choices[num]}" ]] && choices[num]="" || choices[num]="+"
done
printf "You selected"; msg=" nothing"
for i in ${!options[#]}; do
[[ "${choices[i]}" ]] && { printf " %s" "${options[i]}"; msg=""; }
done
echo "$msg"
}
That's still rather inscrutable, but at least it works more or less sanely.

Related

Script enters all conditionals

I'd like to write a script that takes 2 parameters,
The first is parameter that could be "alpha/beta/tcp_friendliness/fast_convergence".
The Second should be a number for the alpha/beta cases and a 0/1 for the other 2.
for example: ./manage_cc alpha 512
Now i've wrote the following script that supposedly covers my cases, but it seems to go into all the conditionals. surely my syntax is broken so any help would be appreciated.
#!/bin/bash
echo -n $2 > /sys/module/tcp_tuner/parameters/$1
if [["$1" == ""] || ["$2" == ""]]
then
echo "You need to pass a property to modify as a first parameter and a value as the second"
fi
if [["$1" == "alpha"] || ["$1" == "beta"]]
then
echo -n $2 > /sys/module/tcp_tuner/parameters/$1
else
if [["$1" == "tcp_friendliness"] || ["$1" == "fast_convergence"]]
then
if [["$2" != "0"] && ["$2" != "1"]]
then
echo "This parameter only accepts a boolean value (0/1)"
exit 1
else
echo -n $2 > /sys/module/tcp_tuner/parameters/$1
fi
else
echo "The only accepted values for first parameter are alpha/beta/tcp_friendliness/fast_convergence"
exit 1
fi
fi
A rewrite of your code:
#!/usr/bin/env bash
write() {
printf "%s" "$2" > "/sys/module/tcp_tuner/parameters/$1"
}
die() {
echo "$*" >&2
exit 1
}
main() {
[[ -z $2 ]] && die "You need to pass a property to modify as a first parameter and a value as the second"
case $1 in
alpha|beta)
write "$1" "$2"
;;
tcp_friendliness|fast_convergence)
if [[ "$2" == "0" || "$2" == "1" ]]; then
write "$1" "$2"
else
die "This parameter only accepts a boolean value (0/1)"
fi
;;
*) die "The only accepted values for first parameter are alpha/beta/tcp_friendliness/fast_convergence"
;;
esac
}
main "$#"
Your condition syntax is wrong. When you start a condition with [[ you have to end it with ]], not just ]. They don't nest like parentheses.
if [[ "$1" == "alpha" || "$1" == "beta" ]]

How can I make this multi-select Bash script default to all items selected?

I modified a great script I found online by Nathan Davieau on serverfault.com, but I have hit the limit to my knowledge of Bash!
It works great, apart from I have to select each item, I want it to start with all items selected and have to deselect them.
#!/bin/bash
ERROR=" "
declare -a install
declare -a options=('php' 'phpmyadmin' 'php-mysqlnd' 'php-opcache' 'mariadb-server' 'sendmail')
#=== FUNCTION ========================================================================
# NAME: ACTIONS
# DESCRIPTION: Actions to take based on selection
# PARAMETER 1:
#=======================================================================================
function ACTIONS() {
for ((i = 0; i < ${#options[*]}; i++)); do
if [[ "${choices[i]}" == "+" ]]; then
install+=(${options[i]})
fi
done
echo "${install[#]}"
}
#=== FUNCTION ========================================================================
# NAME: MENU
# DESCRIPTION: Ask for user input to toggle the name of the plugins to be installed
# PARAMETER 1:
#=======================================================================================
function MENU() {
echo "Which packages would you like to be installed?"
echo
for NUM in "${!options[#]}"; do
echo "[""${choices[NUM]:- }""]" $((NUM + 1))") ${options[NUM]}"
done
echo "$ERROR"
}
#Clear screen for menu
clear
#Menu loop
while MENU && read -e -p "Select the desired options using their number (again to uncheck, ENTER when done): " -n2 SELECTION && [[ -n "$SELECTION" ]]; do
clear
if [[ "$SELECTION" == *[[:digit:]]* && $SELECTION -ge 1 && $SELECTION -le ${#options[#]} ]]; then
((SELECTION--))
if [[ "${choices[SELECTION]}" == "+" ]]; then
choices[SELECTION]=""
else
choices[SELECTION]="+"
fi
ERROR=" "
else
ERROR="Invalid option: $SELECTION"
fi
done
ACTIONS
Here you go, you only need to initialize the choices[] (array) before you actually process them. (re-)Revised code below (with thanks to Charles Duffy) :
#!/bin/bash
ERROR=" "
declare -a install
declare -a options=('php' 'phpmyadmin' 'php-mysqlnd' 'php-opcache' 'mariadb-server' 'sendmail')
############### HERE's THE NEW BIT #################
declare -a choices=( "${options[#]//*/+}" )
####################################################
#=== FUNCTION ========================================================================
# NAME: ACTIONS
# DESCRIPTION: Actions to take based on selection
# PARAMETER 1:
#=======================================================================================
function ACTIONS() {
for ((i = 0; i < ${#options[*]}; i++)); do
if [[ "${choices[i]}" == "+" ]]; then
install+=(${options[i]})
fi
done
echo "${install[#]}"
}
#=== FUNCTION ========================================================================
# NAME: MENU
# DESCRIPTION: Ask for user input to toggle the name of the plugins to be installed
# PARAMETER 1:
#=======================================================================================
function MENU() {
echo "Which packages would you like to be installed?"
echo
for NUM in "${!options[#]}"; do
echo "[""${choices[NUM]:- }""]" $((NUM + 1))") ${options[NUM]}"
done
echo "$ERROR"
}
#Clear screen for menu
clear
#Menu loop
while MENU && read -e -p "Select the desired options using their number (again to uncheck, ENTER when done): " -n2 SELECTION && [[ -n "$SELECTION" ]]; do
clear
if [[ "$SELECTION" == *[[:digit:]]* && $SELECTION -ge 1 && $SELECTION -le ${#options[#]} ]]; then
((SELECTION--))
if [[ "${choices[SELECTION]}" == "+" ]]; then
choices[SELECTION]=""
else
choices[SELECTION]="+"
fi
ERROR=" "
else
ERROR="Invalid option: $SELECTION"
fi
done
ACTIONS
Sorry, but I can't afford the time to give a blow by blow description of how all this works. In this case, the traditional shell debugging tool of set -vx (paired with set +vx) might be a challenge to interpret for a newbie. Experiment only when you can spare the time.
Note that the key bit of code is where the + gets toggled :
if [[ "${choices[SELECTION]}" == "+" ]]; then
choices[SELECTION]=""
else
choices[SELECTION]="+"
fi
IHTH

bash select multiple answers at once

I have a flat file called items that I want to populate a select but I want to be able to choose multiple items at one time.
contents of items file:
cat 1
dog 1
pig 1
cherry 2
apple 2
Basic script:
#!/bin/bash
PS3=$'\n\nSelect the animals you like: '
options=$(grep '1' items|grep -v '^#' |awk '{ print $1 }')
select choice in $options
do
echo "you selected: $choice"
done
exit 0
The way it flows now is I can only select one option at at time. I'd like to be able to answer 1,3 or 1 3 and have it respond "you selected: cat pig"
Thank you,
Tazmarine
I can offer a somewhat different approach that uses a different selection prompt style. Here's a bash function that allows user to select multiple options with arrow keys and Space, and confirm with Enter. It has a nice menu-like feel. I wrote it with the help of https://unix.stackexchange.com/a/415155. It can be called like this:
multiselect result "Option 1;Option 2;Option 3" "true;;true"
The result is stored as an array in a variable with the name supplied as the first argument. Last argument is optional and is used for making some options selected by default. It looks like this:
function prompt_for_multiselect {
# little helpers for terminal print control and key input
ESC=$( printf "\033")
cursor_blink_on() { printf "$ESC[?25h"; }
cursor_blink_off() { printf "$ESC[?25l"; }
cursor_to() { printf "$ESC[$1;${2:-1}H"; }
print_inactive() { printf "$2 $1 "; }
print_active() { printf "$2 $ESC[7m $1 $ESC[27m"; }
get_cursor_row() { IFS=';' read -sdR -p $'\E[6n' ROW COL; echo ${ROW#*[}; }
key_input() {
local key
IFS= read -rsn1 key 2>/dev/null >&2
if [[ $key = "" ]]; then echo enter; fi;
if [[ $key = $'\x20' ]]; then echo space; fi;
if [[ $key = $'\x1b' ]]; then
read -rsn2 key
if [[ $key = [A ]]; then echo up; fi;
if [[ $key = [B ]]; then echo down; fi;
fi
}
toggle_option() {
local arr_name=$1
eval "local arr=(\"\${${arr_name}[#]}\")"
local option=$2
if [[ ${arr[option]} == true ]]; then
arr[option]=
else
arr[option]=true
fi
eval $arr_name='("${arr[#]}")'
}
local retval=$1
local options
local defaults
IFS=';' read -r -a options <<< "$2"
if [[ -z $3 ]]; then
defaults=()
else
IFS=';' read -r -a defaults <<< "$3"
fi
local selected=()
for ((i=0; i<${#options[#]}; i++)); do
selected+=("${defaults[i]}")
printf "\n"
done
# determine current screen position for overwriting the options
local lastrow=`get_cursor_row`
local startrow=$(($lastrow - ${#options[#]}))
# ensure cursor and input echoing back on upon a ctrl+c during read -s
trap "cursor_blink_on; stty echo; printf '\n'; exit" 2
cursor_blink_off
local active=0
while true; do
# print options by overwriting the last lines
local idx=0
for option in "${options[#]}"; do
local prefix="[ ]"
if [[ ${selected[idx]} == true ]]; then
prefix="[x]"
fi
cursor_to $(($startrow + $idx))
if [ $idx -eq $active ]; then
print_active "$option" "$prefix"
else
print_inactive "$option" "$prefix"
fi
((idx++))
done
# user key control
case `key_input` in
space) toggle_option selected $active;;
enter) break;;
up) ((active--));
if [ $active -lt 0 ]; then active=$((${#options[#]} - 1)); fi;;
down) ((active++));
if [ $active -ge ${#options[#]} ]; then active=0; fi;;
esac
done
# cursor position back to normal
cursor_to $lastrow
printf "\n"
cursor_blink_on
eval $retval='("${selected[#]}")'
}
You can not do that as such, but you can always record each individual selection:
#!/bin/bash
PS3=$'\n\nSelect the animals you like: '
options=$(grep '1' items|grep -v '^#' |awk '{ print $1 }')
# Array for storing the user's choices
choices=()
select choice in $options Finished
do
# Stop choosing on this option
[[ $choice = Finished ]] && break
# Append the choice to the array
choices+=( "$choice" )
echo "$choice, got it. Any others?"
done
# Write out each choice
printf "You selected the following: "
for choice in "${choices[#]}"
do
printf "%s " "$choice"
done
printf '\n'
exit 0
Here's an example interaction:
$ ./myscript
1) cat
2) dog
3) pig
4) Finished
Select the animals you like: 3
pig, got it. Any others?
Select the animals you like: 1
cat, got it. Any others?
Select the animals you like: 4
You selected the following: pig cat
If you instead want to be able to write 3 1 on the same line, you'll have to make your own menu loop with echo and read
This is what I came up with. This seems to works as I want. I want the final output to be comma separated:
#!/bin/bash
newarray=(all $(grep '1' items|grep -v '^#' |awk '{ print $1 }'))
options() {
num=0
for i in ${newarray[#]}; do
echo "$num) $i"
((num++))
done
}
getanimal() {
while [[ "$show_clean" =~ [A-Za-z] || -z "$show_clean" ]]; do
echo "What animal do you like: "
options
read -p 'Enter number: ' show
echo
show_clean=$(echo $show|sed 's/[,.:;]/ /g')
selected=$(\
for s in $show_clean; do
echo -n "\${newarray[${s}]},"
done)
selected_clean=$(echo $selected|sed 's/,$//')
done
eval echo "You selected $selected_clean"
}
getanimal
exit 0

zsh - read user input with default value - empty input not empty?

I wrote a function that asks for user input like this:
function is_confirmed {
read -rs -k 1 ans
if [[ "${ans}" == "n" || "${ans}" == "N" ]]; then
printf "No\n"
return 1
fi
if [[ "${ans}" == "y" || "${ans}" == "Y" ]]; then
printf "Yes\n"
return 0
fi
# here is my actual problem!!! this doesnt work when user input is blank!
if [[ "${ans}" == "" ]]; then
printf "Yes!\n"
return 1
fi
# Output is Damn!
printf "Damn"
return 1
}
works great so far, however, I want to set "yes" as the default answear, so when the user inputs nothing and just presses enter, it should fall back to "yes", so I tried it with || "$ans" == "" but that still falls back to "Damn"
how come? When I echo $ans at the end of the function it is empty...
EDIT 1:
This is what happens:
e_ask "Are you sure you want to install?\nWarning: This may override some files in your home directory."
if is_confirmed; then
echo "Great!"
else
e_error "Aborting..."
fi
here are the functions:
function e_ask {
printf "\n$1\n"
printf "(Y/n): "
}
function e_warn {
printf "Warning: $1\n"
}
function e_error {
printf "Error: $1\n"
exit 1
}
A case is better when you want to consider many different values and a default one
function is_confirmed {
read -rs -k 1 ans
case "${ans}" in
y|Y|$'\n')
printf "Yes\n"
return 0
;;
*) # This is the default
printf "No\n"
return 1
esac
}
Just add default variable in the beginning of function:
function is_confirmed {
ans="y"
echo "Put your choice:"
read $ans
}
If nothing is typed in, default value will remain.
The problem lies in how you wrote your conditions using the [[ builtin.
You need to change your conditions to:
function is_confirmed {
read -rs -k 1 ans
if [[ "${ans}" == "n" ]] || [[ "${ans}" == "N" ]]; then
printf "No\n"
return 1
else if [[ "${ans}" == "y" ]] || [[ "${ans}" == "Y" ]]; then
printf "Yes\n"
return 0
else if [[ -z "${ans}" ]]; then
printf "Empty\n"
else
printf "Damn\n"
fi
return 1
}
To explain, the [[ builtin tests just one condition, but you can chain multiple instances of [[ with && and ||. Your code, in contrast, tried to test 2 conditions inside [[ and used || as it's C/C++ usage.
More details available in help [[ or man bash

case or If string contains word1 and word2

In this example I want it to know if $# contains two words/symbols "load" and "/"
for one word/symbol this works
case "$#" in */*)
;;
echo "going to do stuff"
*)
echo "will do something else"
;;
esac
or
string='My string';
if [[ "$string" == *My* ]]
then
echo "It's there!";
fi
But if two words/symbols appear at random places I cant figure out how to do it.
Update:
The input will the module command. In this case I want to know if it is the module load with or without / that indicate version. the command will look like this
1) module load appname/1.1.1 or
2) module load appname
3) module (not load) (list, avail etc)
It is number 1 I am interested in for now.
3 will in some cases be variation of 1.
2 will be run as is but will include a message to the user
The slow way would be to iterate through the array twice and then check if both "load" and "/" were present, like this:
for element in $#; do [[ "$element" == "load" ]] && loadPresent=1; done
for element in $#; do [[ "$element" =~ ".*/.*" ]] && slashPresent=1; done
if [[ $loadPresent == 1 ]] && [[ $slashPresent == 1 ]]; then
echo "Contains load and /"
fi
(As I interpreted your question you want one parameter to be exactly "load" and another one to contain a slash.)
Something like this is possible:
if [[ ${#} =~ .*/.* && ${#} =~ ((^)|([ ]))load(($)|([ ])) ]]
then
echo both
fi
-or-
if LOAD=0 && SLASH=0 && \
for ARG in ${#};
do
if [ "${ARG#*/}" != "${ARG}" ]; then SLASH=1; fi
if [ "${ARG}" = "load" ]; then LOAD=1; fi
done && [ "${LOAD}${SLASH}" = "11" ];
then
echo both
fi
-or-
function loadslash()
{
LOAD=0 && SLASH=0
for ARG in ${#};
do
if [ "${ARG#*/}" != "${ARG}" ]; then SLASH=1; fi
if [ "${ARG}" = "load" ]; then LOAD=1; fi
done
test "${LOAD}${SLASH}" = "11"
}
if loadslash ${#}
then
echo both
fi
this will satisfy your requirements
if [[ $1 == "load" ]]; then
if [[ $2 == */* ]]; then
do first case
else
do second case
fi
else
do third case
fi

Resources