Checkboxes with bash script - bash

I am trying to do a very simple bash script which emulates the behavior of checkboxes in appearance!
I want it to show some options and move the cursor to the next checkbox according to the key press of the left or right arrow keys. I've already managed to do this using READ and Ansii escape sequences to detect the arrow keys and I use tput to move the cursor.
My problem with this it's that I need to read a certain letter (x for instance) to be pressed and then take another action. But how can I detect this key press and at the same time detect if an arrow key it's being pressed or not?
To detect the ansii codes I need to read 3 characters and with the X character (the key to "select") I need to read just one, how can I read 3 characters and at the same time read just one?
Also I've been trying to make something so the user can JUST press the left or right arrow keys or the x key but if he presses ANY other key, nothing should happen!
I've done this this far:
#!/bin/bash
## Here I just print in the screen the "form" with the "checkboxes"
function screen_info(){
clear
cat <<EOF
/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_
||
|| 1[ ] 2[ ] 3[ ]
||
#############################
EOF
}
## This function detects the arrow keys and moves the cursor
function arrows(){
## I use ANSII escape sequences to detect the arrow keys
left_arrow=$'\x1b\x5b\x44' #leftuierda
right_arrow=$'\x1b\x5b\x43' #rightecha
just_x_key=""
## With tput I move the cursor accordingly
cursor_x=14
cursor_y=3
tput cup $cursor_y $cursor_x
while [ -z "$just_x_key" -o "$just_x_key" != "$just_x_key" ]; do
read -s -n3 key
while [ `expr length "$key"` -ne 3 ]; do
key=""
read -s -n3 key
break
done
case "$key" in
$left_arrow)
if [ $cursor_x -gt 14 ]; then
cursor_x=`expr $cursor_x - 8`
fi
tput cup $cursor_y $cursor_x
#This is supposed to be a simple condition detecting the x key pressed which I want to trigger something... But how to read a single character to this condition and 3 characters at the same time if the user presses the arrow key ???? =/
#read -s just_x_key
#if [ $just_x_key == x ]; then
# echo X
# tput cup 7 15
# echo "YOU PRESSED THE RIGHT KEY!!! =D"
#fi
;;
$right_arrow)
if [ $cursor_x -lt 28 ]; then
cursor_x=`expr $cursor_x + 8`
fi
tput cup $cursor_y $cursor_x
#read -s just_x_key
#if [ $just_x_key == x ]; then
# echo X
# tput cup 7 15
# echo "YOU PRESSED THE RIGHT KEY!!! =D"
#fi
;;
esac
done
exit $?
}
#EXECUTION
#=========
## I just call the functions!
screen_info
arrows
Yes I know, it's not the most perfect code ever but I'm trying to learn. Suggestions will be very appreciated.

If you just want the checkboxes in a script, you can use the whiptail(1) or dialog(1) tools to create the checkboxes:
$ whiptail --checklist "Please pick one" 10 60 5 one one off two two off\
three three off four four off five five off
┌──────────────────────────────────────────────────────────┐
│ [ ] one one │
│ [*] two two │
│ [ ] three three │
│ [*] four four │
│ [ ] five five │
│ │
│ <Ok> <Cancel> │
│ │
└──────────────────────────────────────────────────────────┘
"two" "four"$
The final "two" "four" is the selected entries returned from the whiptail(1) program.
If you're programming this for the fun of it yourself, let me know and I'll just convert this to a comment in the hopes that someone else will find the hint useful in the future.

Alternatively, you can use my library at
https://sites.google.com/site/easybashgui
It detects automatically if dialog or whiptail is installed.
(Normally, at least one of two is always present even in bare systems...)
So your scripts becomes:
source easybashgui; menu 1 2 3 ; clean_temp
Why reinventing the wheel?
;P

Related

Bash error code doesn't show in my shell prompt [duplicate]

I've been trying to customize my Bash prompt so that it will look like
[feralin#localhost ~]$ _
with colors. I managed to get constant colors (the same colors every time I see the prompt), but I want the username ('feralin') to appear red, instead of green, if the last command had a nonzero exit status. I came up with:
\e[1;33m[$(if [[ $? == 0 ]]; then echo "\e[0;31m"; else echo "\e[0;32m"; fi)\u\e[m#\e[1;34m\h \e[0;35m\W\e[1;33m]$ \e[m
However, from my observations, the $(if ...; fi) seems to be evaluated once, when the .bashrc is run, and the result is substituted forever after. This makes the name always green, even if the last exit code is nonzero (as in, echo $?). Is this what is happening? Or is it simply something else wrong with my prompt? Long question short, how do I get my prompt to use the last exit code?
As you are starting to border on a complex PS1, you might consider using PROMPT_COMMAND. With this, you set it to a function, and it will be run after each command to generate the prompt.
You could try the following in your ~/.bashrc file:
PROMPT_COMMAND=__prompt_command # Function to generate PS1 after CMDs
__prompt_command() {
local EXIT="$?" # This needs to be first
PS1=""
local RCol='\[\e[0m\]'
local Red='\[\e[0;31m\]'
local Gre='\[\e[0;32m\]'
local BYel='\[\e[1;33m\]'
local BBlu='\[\e[1;34m\]'
local Pur='\[\e[0;35m\]'
if [ $EXIT != 0 ]; then
PS1+="${Red}\u${RCol}" # Add red if exit code non 0
else
PS1+="${Gre}\u${RCol}"
fi
PS1+="${RCol}#${BBlu}\h ${Pur}\W${BYel}$ ${RCol}"
}
This should do what it sounds like you want. Take a look a my bashrc's sub file if you want to see all the things I do with my __prompt_command function.
If you don't want to use the prompt command there are two things you need to take into account:
getting the value of $? before anything else. Otherwise it'll be overridden.
escaping all the $'s in the PS1 (so it's not evaluated when you assign it)
Working example using a variable
PS1="\$(VALU="\$?" ; echo \$VALU ; date ; if [ \$VALU == 0 ]; then echo zero; else echo nonzero; fi) "
Working example without a variable
Here the if needs to be the first thing, before any command that would override the $?.
PS1="\$(if [ \$? == 0 ]; then echo zero; else echo nonzero; fi) "
Notice how the \$() is escaped so it's not executed right away, but each time PS1 is used. Also all the uses of \$?.
Compact solution:
PS1='... $(code=${?##0};echo ${code:+[error: ${code}]})'
This approach does not require PROMPT_COMMAND (apparently this can be slower sometimes) and prints [error: <code>] if the exit code is non-zero, and nothing if it's zero:
... > false
... [error: 1]> true
... >
Change the [error: ${code}] part depending on your liking, with ${code} being the non-zero code to print.
Note the use of ' to ensure the inline $() shell gets executed when PS1 is evaluated later, not when the shell is started.
As bonus, you can make it colorful in red by adding \e[01;31m in front and \e[00m after to reset:
PS1='... \e[01;31m$(code=${?##0};echo ${code:+[error: ${code}]})\e[00m'
--
How it works:
it uses bash parameter substitution
first, the ${?##0} will read the exit code $? of the previous command
the ## will remove any 0 pattern from the beginning, effectively making a 0 result an empty var (thanks #blaskovicz for the trick)
we assign this to a temporary code variable as we need to do another substitution, and they can't be nested
the ${code:+REPLACEMENT} will print the REPLACEMENT part only if the variable code is set (non-empty)
this way we can add some text and brackets around it, and reference the variable again inline: [error: ${code}]
I wanted to keep default Debian colors, print the exact code, and only print it on failure:
# Show exit status on failure.
PROMPT_COMMAND=__prompt_command
__prompt_command() {
local curr_exit="$?"
local BRed='\[\e[0;91m\]'
local RCol='\[\e[0m\]'
PS1='${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u#\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '
if [ "$curr_exit" != 0 ]; then
PS1="[${BRed}$curr_exit${RCol}]$PS1"
fi
}
The following provides a leading green check mark when the exit code is zero and a red cross in all other cases. The remainder is a standard colorized prompt. The printf statements can be modified to present the two states that were originally requested.
PS1='$(if [ $? -eq 0 ]; then printf "\033[01;32m""\xE2\x9C\x93"; else printf "\033[01;31m""\xE2\x9C\x95"; fi) \[\e[00;32m\]\u#\h\[\e[00;30m\]:\[\e[01;33m\]\w\[\e[01;37m\]\$ '
Why didn't I think about that myself? I found this very interesting and added this feature to my 'info-bar' project. Eyes will turn red if the last command failed.
#!/bin/bash
eyes=(O o ∘ ◦ ⍤ ⍥) en=${#eyes[#]} mouth='_'
face () { # gen random face
[[ $error -gt 0 ]] && ecolor=$RED || ecolor=$YLW
if [[ $1 ]]; then printf "${eyes[$[RANDOM%en]]}$mouth${eyes[$[RANDOM%en]]}"
else printf "$ecolor${eyes[$[RANDOM%en]]}$YLW$mouth$ecolor${eyes[$[RANDOM%en]]}$DEF"
fi
}
info () { error=$?
[[ -d .git ]] && { # If in git project folder add git status to info bar output
git_clr=('GIT' $(git -c color.ui=always status -sb)) # Colored output 4 info
git_tst=('GIT' $(git status -sb)) # Simple output 4 test
}
printf -v line "%${COLUMNS}s" # Set border length
date=$(printf "%(%a %d %b %T)T") # Date & time 4 test
test=" O_o $PWD ${git_tst[*]} $date o_O " # Test string
step=$[$COLUMNS-${#test}]; [[ $step -lt 0 ]] && step=0 # Count spaces
line="$GRN${line// /-}$DEF\n" # Create lines
home="$BLD$BLU$PWD$DEF" # Home dir info
date="$DIM$date$DEF" # Colored date & time
#------+-----+-------+--------+-------------+-----+-------+--------+
# Line | O_o |homedir| Spaces | Git status | Date| o_O | Line |
#------+-----+-------+--------+-------------+-----+-------+--------+
printf "$line $(face) $home %${step}s ${git_clr[*]} $date $(face) \n$line" # Final info string
}
PS1='${debian_chroot:+($debian_chroot)}\n$(info)\n$ '
case "$TERM" in xterm*|rxvt*)
PS1="\[\e]0;${debian_chroot:+($debian_chroot)} $(face 1) \w\a\]$PS1";;
esac
Improved demure answer:
I think this is important because the exit status is not always 0 or 1.
if [ $EXIT != 0 ]; then
PS1+="${Red}${EXIT}:\u${RCol}" # Add red if exit code != 0
else
PS1+="${Gre}${EXIT}:\u${RCol}" # Also displays exit status
fi
To preserve the original prompt format (not just colors),
you could append following to the end of file ~/.bashrc:
PS1_ORIG=$PS1 # original primary prompt value
PROMPT_COMMAND=__update_prompt # Function to be re-evaluated after each command is executed
__update_prompt() {
local PREVIOUS_EXIT_CODE="$?"
if [ $PREVIOUS_EXIT_CODE != 0 ]; then
local RedCol='\[\e[0;31m\]'
local ResetCol='\[\e[0m\]'
local replacement="${RedCol}\u${ResetCol}"
# Replace username color
PS1=${PS1_ORIG//]\\u/]$replacement}
## Alternative: keep same colors, append exit code
#PS1="$PS1_ORIG[${RedCol}error=$PREVIOUS_EXIT_CODE${ResetCol}]$ "
else
PS1=$PS1_ORIG
fi
}
See also the comment about the alternative approach that preserves username color and just appends an error code in red to the end of the original prompt format.
You can achieve a similar result to include a colored (non-zero) exit code in a prompt, without using subshells in the prompt nor prompt_command.
You color the exit code portion of the prompt, while having it only appear when non-zero.
Core 2$ section of the prompt: \\[\\033[0;31;4m\\]\${?#0}\\[\\033[0;33m\\]\$ \\[\\033[0m\\]
Key elements:
return code, if not 0: \${?#0} (specificly "removes prefix of 0")
change color without adding to calculated prompt-width: \\[\\033[0;31m\\]
\\[ - begin block
\\033 - treat as 0-width, in readline calculations for cmdline editing
[0;31;4m - escape code, change color, red fg, underline
\\] - end block
Components:
\\[\\033[0;31;4m\\] - set color 0;31m fg red, underline
\${?#0} - display non-zero status (by removing 0 prefix)
\\[\\033[0;33m\\] - set color 0;33m fg yellow
\$ - $ or # on EUID
\\[\\033[0m\\] - reset color
The full PS1 I use (on one host):
declare -x PS1="\\[\\033[0;35m\\]\\h\\[\\033[1;37m\\] \\[\\033[0;37m\\]\\w \\[\\033[0;33m\\]\\[\\033[0;31;4m\\]\${?#0}\\[\\033[0;33m\\]\$ \\[\\033[0m\\]"
Note: this addresses a natural extension to this question, in a more enduring way then a comment.
Bash
function my_prompt {
local retval=$?
local field1='\u#\h'
local field2='\w'
local field3='$([ $SHLVL -gt 1 ] && echo \ shlvl:$SHLVL)$([ \j -gt 0 ] && echo \ jobs:\j)'"$([ ${retval} -ne 0 ] && echo \ exit:$retval)"
local field4='\$'
PS1=$'\n'"\e[0;35m${field1}\e[m \e[0;34m${field2}\e[m\e[0;31m${field3}\e[m"$'\n'"\[\e[0;36m\]${field4}\[\e[m\] "
}
PROMPT_COMMAND="my_prompt; ${PROMPT_COMMAND}"
Zsh
PROMPT=$'\n''%F{magenta}%n#%m%f %F{blue}%~%f%F{red}%(2L. shlvl:%L.)%(1j. jobs:%j.)%(?.. exit:%?)%f'$'\n''%F{cyan}%(!.#.$)%f '
Images of prompt

Loop in a menu untill a condition or the exit/log out option is selected

I'm writing a bash script where an user can add a new database.
The script needs to check, if the name introduced by the user is valid and after if a database with the same name already exist.
For this I already created 2 functions.
is_valid_name , returns 0 if is valid, 1 if is not valid
is_database, returns 0 if a database with the name introduced by the user exist, and 1 if doesn't exist
I want to offer the user the possibility, in case the name is not valid, or the database already exist, to add a different name or cancel/exit.
I want to do this using a Menu with 2 options:
Add a new database
Exit
The pseudo-code:
-> A:
echo Add a database
read -r $database # get the database name from the user
check the entered name - `is_valid_name'
Branch 1.
if the name is not valid, is_valid_name will show the error and return 1
-> B:
show the Menu with the two options
if the user select Option 1(Add a new database) go back to A(see above in bold)
if the user select option 2(Exit) exist the script
Branch 2
if the name is valid check if database exist, is_database
Branch 2.1
if database exist show the Menu with the two options, go back to B(see above in bold)
Branch 2.2
if database doesn't exist go further and execute other code, like create database, populate database etc
I'm thinking of using a while do loop to check if both the name and exist for the database, and get out of the loop if both are ok and continue the code or if user wants to exist.
I don't know how to set(not as syntax) the loop to catch both conditions
If I understood correctly something like this will do the job:
#!/usr/bin/env bash
function is_entity()
{
# fill in to do what you want
printf "%s\n" "$1"
}
while true
do
echo Add New Entity:
read -r entity
if is_entity "$entity"; then
select TASK in 'Entity exist, use another name' 'Logout'
do
case "$REPLY" in
1)
continue 2
;;
2)
printf "Bye\n"
exit 0
;;
esac
done
fi
done
To begin with, do not use all-uppercase variable names -- those are generally reserved for system use. (it's not that you can't, it's just bad form). Use lower-case variable names for your user-variables.
While it is not 100% clear what the remainder of your script should do, it looks as if you are trying to build a list without duplicates using is_entity() to check whether that 'entity' already exists and returning 0 if it does or 1 if it does not. That part is clear -- what isn't clear is how to make the explanation of how to do it useful to you for the rest of your script.
Let's look at it this way, to check whether one entity exists, there must be a collection of them somewhere. For bash, an array of them make sense. So to check whether an entity already exists within an array, you can do something similar to:
declare -a entity # declare an empty indexed array to hold entities
logout=0 # a flag to handle your 'logout' entry
## check if first argument in entity array
# return 0 if it exists, 1 otherwise
is_entity() {
for i in "${entity[#]}" # loop over array comparing entries
do
[ "$i" = "$1" ] && return 0 # if found, return 0
done
return 1 # otherwise return 1
}
That provides a simple function to check whether a previous element in your entity array is present given the first argument to the function (error handling if no argument is given is left to you)
If you are going to have an array of entities, you will need a way to add them. A second simple add_entity() function can call your is_entity() function and either return 0 if the name chosen is already in the array, or if not, just add the new name to the array and display a slightly different menu letting you know that the entity was "Added" instead of "Exists". Something simple like the following will work:
## add entity to array
# return 0 if it exists, 1 otherwise
add_entity () {
local name
printf "\nenter name: " # prompt for new entity name
read name
is_entity "$name" # check if it exists with is_entity
if [ $? -eq '0' ]
then
return 0 # if so, return 0
else
entity+=( "$name" ) # otherwise add it to array
fi
return 1 # and return 1
}
(note: the use of local for name which insures the name variable is limited to the scope of the function and is unset when the function returns)
The remainder of your script to display either the "Added" menu or your "Exists" menu with the two-choices to either add another (or choose another name) could be implemented with two case statements based on the return from add_entity(). Essentially you will loop continually until logout is chosen, calling add_entity() at the beginning of the loop and then using a case statement based on the return value to determine which menu to display. An outline of the logic would be something like:
while [ "$logout" -eq '0' ] ## main loop -- loop until logout -ne 0
do
add_entity # prompt and check with add_entity/is_entity
case "$?" in # filter return with case
0 ) # if the entered name already existed
## Existed Menu
1 ) # if the entity did not exist, but was added to array
## Added Menu
esac
done
In each case, your "Existed" or "Added" Menu could use a simple select loop and could be something like the following for you "Exists" case:
printf "\nEntity exists - '%s'\n" "${entity[$((${#entity[#]}-1))]}"
select task in "use another name" "logout" # display exists menu
do
case "$task" in
"use another name" ) # select menu matches string
break
;;
"logout" )
logout=1 # set logout flag to break outer loop
break;
;;
"" ) # warn on invalid input
printf "invalid choice\n" >&2
;;
esac
done
;;
To verify the operation and that your entities were collected, you could simply display the contents of the array after you exit the loop, e.g.
printf "\nthe entities in the array are:\n"
for ((i = 0; i < ${#entity[#]}; i++))
do
printf " entity[%2d] %s\n" "$i" "${entity[i]}"
done
Putting all the pieces of the puzzle together, you could handle your logic and display the appropriate menu with a script similar to:
#!/bin/bash
declare -a entity # declare an empty indexed array to hold entities
logout=0 # a flag to handle your 'logout' entry
## check if first argument in entity array
# return 0 if it exists, 1 otherwise
is_entity() {
for i in "${entity[#]}" # loop over array comparing entries
do
[ "$i" = "$1" ] && return 0 # if found, return 0
done
return 1 # otherwise return 1
}
## add entity to array
# return 0 if it exists, 1 otherwise
add_entity () {
local name
printf "\nenter name: " # prompt for new entity name
read name
is_entity "$name" # check if it exists with is_entity
if [ $? -eq '0' ]
then
return 0 # if so, return 0
else
entity+=( "$name" ) # otherwise add it to array
fi
return 1 # and return 1
}
while [ "$logout" -eq '0' ] ## main loop -- loop until logout -ne 0
do
add_entity # prompt and check with add_entity/is_entity
case "$?" in # filter return with case
0 ) # if the entered name already existed
printf "\nEntity exists - '%s'\n" "${entity[$((${#entity[#]}-1))]}"
select task in "use another name" "logout" # display exists menu
do
case "$task" in
"use another name" ) # select menu matches string
break
;;
"logout" )
logout=1 # set logout flag to break outer loop
break;
;;
"" ) # warn on invalid input
printf "invalid choice\n" >&2
;;
esac
done
;;
1 ) # if the entity did not exist, but was added to array
printf "\nEntity added - '%s'\n" "${entity[$((${#entity[#]}-1))]}"
select task in "Add another" "logout" # display added menu
do
case "$task" in
"Add another" )
break
;;
"logout" )
logout=1
break
;;
"" )
printf "invalid choice\n" >&2
;;
esac
done
;;
esac
done
printf "\nthe entities in the array are:\n"
for ((i = 0; i < ${#entity[#]}; i++))
do
printf " entity[%2d] %s\n" "$i" "${entity[i]}"
done
Example Use/Output
Running the script to verify your menus and provide testing of the scripts response to different inputs, you could do something like:
$ bash ~/tmp/entity_exists.sh
enter name: one
Entity added - 'one'
1) Add another
2) logout
#? 1
enter name: one
Entity exists - 'one'
1) use another name
2) logout
#? crud!
invalid choice
#? 1
enter name: two
Entity added - 'two'
1) Add another
2) logout
#? 1
enter name: three
Entity added - 'three'
1) Add another
2) logout
#? 2
the entities in the array are:
entity[ 0] one
entity[ 1] two
entity[ 2] three
Look things over and let me know if you have further questions. It was a bit difficult to just tell you how to check is_entity() without knowing how you have them stored to begin with, but the logic here can be adapted to any number of different circumstances.

How do I chain multiple user prompts together with the ability to go back a prompt?

I am wondering how I can make a bash script that has multiple menus in it.
For example, here's what the user would see upon running it:
Type the number of choosing:
1-play
2-load
3-exit
1
What is your name:
::prev::
Type the number of choosing:
1-play
2-load
3-exit
1
What is your name:
Brad
Where are you from, Brad?
Pennsylvania
What is your favourite colour?
1-blue
2-red
3-green
4-grey
5-magenta
,sdfhljalk:J;
What is your favourite colour?
1-blue
2-red
3-green
4-grey
5-magenta
2
What is your favourite toy?
train
What would you like on your sandwich?
::prev::
What is your favourite toy?
~`!##$%^& * ()_+=-{}[]|\"':;?/>.<,
What is your favourite toy?
::exit::
Exiting....
I apologize for it being long, I just want to cover all bases for the new game I'm going to be making. I want this to be the question to end all questions.
I want to be able to type ::prev:: wherever I am and have it go back to the previous question, and I'd like ::exit:: to exit the script wherever it is. Also, I'd like unrecognized input during questions with numbered responses to just reload the question without continuing, and for input containing characters that may cause a script break (something like :;!## ...) to reload the question instead of breaking.
Any help is greatly appreciated!
By the way, I'm using OS X Yosemite
First thing to do in this situation is to try and think of how, generally, you could implement something like this. Probably the biggest addition to complexity is using ::prev:: to go back a question. This means we need to represent the application state in some way such that we can move forward or backward.
Luckily, this is pretty simple: it's basically just an implementation of a stack that we need. Some visuals:
...
<--pop-- <--pop-- Location prompt <--pop-- ...
Name prompt Name prompt ...
Main menu --push-> Main menu --push-> Main menu --push-> ...
This also means each individual piece of the program needs to be self-contained. We can easily do this in shell scripting with functions.
So we need several pieces:
Function which displays a menu and allows the user to choose a value.
Function which displays a text prompt and allows the user to choose a value.
Function which manages a stack that represents the state of the program.
Individual functions for each piece of the program.
Let's first write our menu prompt function. This part is pretty easy. Bash will do most of the work using the select loop, which prints a menu for us. We'll just wrap it so that we can handle custom logic, like expecting ::exit:: or ::prev:: and some pretty-printing.
function show_menu {
echo "$1" # Print the prompt
PS3='> ' # Set prompt string 3 to '> '
select selection in "${menu_items[#]}" # Print menu using the menu_items array
do
if [[ "$REPLY" =~ ^(::exit::|::prev::)$ ]]; then
# If the user types ::exit:: or ::prev::, exit the select loop
# and return 1 from the function, with $selection containing
# the command the user entered.
selection="$REPLY"
return 1
fi
# $selection will be blank if the user did not choose a valid menu item.
if [ -z "$selection" ]; then
# Display error message if $selection is blank
echo 'Invalid input. Please choose an option from the menu.'
else
# Otherwise, return a success return code.
return 0
fi
done
}
We can now use this function like so:
menu_items=('Item 1' 'Item 2' 'Item 3')
if ! show_menu 'Please choose an item from the menu below.'; then
echo "You entered the command $selection."
fi
echo "You chose $selection."
Great! Now on to the next item on the agenda, writing the code that accepts text input from the user.
# Prompt for a required text value.
function prompt_req {
# do...while
while : ; do
# Print the prompt on one line, then '> ' on the next.
echo "$1"
printf '> '
read -r selection # Read user input into $selection
if [ -z "$selection" ]; then
# Show error if $selection is empty.
echo 'A value is required.'
continue
elif [[ "$selection" =~ ^(::exit::|::prev::)$ ]]; then
# Return non-success return code if ::exit:: or ::prev:: were entered.
return 1
elif [[ "$selection" =~ [^a-zA-Z0-9\'\ ] ]]; then
# Make sure input only contains a whitelist of allowed characters.
# If it has other characters, print an error and retry.
echo "Invalid characters in input. Allowed characters are a-z, A-Z, 0-9, ', and spaces."
continue
fi
# This break statement only runs if no issues were found with the input.
# Exits the while loop and the function returns a success return code.
break
done
}
Great. This function works similarly to the first:
if ! prompt_req 'Please enter a value.'; then
echo "You entered the command $selection."
fi
echo "You entered '$selection'."
Now that we have user input handled, we need to handle the program flow with our stack-managing function. This is fairly easy to implement in bash using an array.
When a part of the program runs and completes, it will ask the flow manager to run the next function. The flow manager will push the name of the next function onto stack, or rather, add it to the end of the array, and then run it. If ::prev:: is entered, it will pop the last function's name off of the stack, or remove the last element of the array, and then run the function before it.
Less talk, more code:
# Program flow manager
function run_funcs {
# Define our "stack" with its initial value being the function name
# passed directly to run_funcs
funcs=("$1")
# do...while
while : ; do
# Reset next_func
next_func=''
# Call the last function name in funcs.
if "${funcs[${#funcs[#]}-1]}"; then
# If the function returned 0, then no :: command was run by the user.
if [ -z "$next_func" ]; then
# If the function didn't set the next function to run, exit the program.
exit 0
else
# Otherwise, add the next function to run to the funcs array. (push)
funcs+=("$next_func")
fi
else
# If the function returned a value other than 0, a command was run.
# The exact command run will be in $selection
if [ "$selection" == "::prev::" ]; then
if [ ${#funcs[#]} -lt 2 ]; then
# If there is only 1 function in funcs, then we can't go back
# because there's no other function to call.
echo 'There is no previous screen to return to.'
else
# Remove the last function from funcs. (pop)
unset funcs[${#funcs[#]}-1]
fi
else
# If $selection isn't ::prev::, then it's ::exit::
exit 0
fi
fi
# Add a line break between function calls to keep the output clean.
echo
done
}
Our run_funcs function expects:
to be called with the name of the first function to run, and
that each function that runs will output the name of the next function to run to next_func if execution of the program must proceed.
Alright. That should be pretty simple to work with. Let's actually write the program now:
function main_menu {
menu_items=('Play' 'Load' 'Exit')
if ! show_menu 'Please choose from the menu below.'; then
return 1
fi
if [ "$selection" == 'Exit' ]; then
exit 0
fi
if [ "$selection" == 'Load' ]; then
# Logic to load game state
echo 'Game loaded.'
fi
next_func='prompt_name'
}
function prompt_name {
if ! prompt_req 'What is your name?'; then
return 1
fi
name="$selection"
next_func='prompt_location'
}
function prompt_location {
if ! prompt_req "Where are you from, $name?"; then
return 1
fi
location="$selection"
next_func='prompt_colour'
}
function prompt_colour {
menu_items=('Blue' 'Red' 'Green' 'Grey' 'Magenta')
if ! show_menu 'What is your favourite colour?'; then
return 1
fi
colour="$selection"
next_func='prompt_toy'
}
function prompt_toy {
if ! prompt_req 'What is your favourite toy?'; then
return 1
fi
toy="$selection"
next_func='print_player_info'
}
function print_player_info {
echo "Your name is $name."
echo "You are from $location."
echo "Your favourite colour is $colour."
echo "Your favourite toy is $toy."
# next_func is not set, so the program will exit after running this function.
}
echo 'My Bash Game'
echo
# Start the program, with main_menu as an entry point.
run_funcs main_menu
Everything is in order now. Let's try out our program!
$ ./bash_game.sh
My Bash Game
Please choose from the menu below.
1) Play
2) Load
3) Exit
> ::prev::
There is no previous screen to return to.
Please choose from the menu below.
1) Play
2) Load
3) Exit
> 2
Game loaded.
What is your name?
> Stephanie
Where are you from, Stephanie?
> ::prev::
What is your name?
> Samantha
Where are you from, Samantha?
> Dubl*n
Invalid characters in input. Allowed characters are a-z, A-Z, 0-9, ', and spaces.
Where are you from, Samantha?
> Dublin
What is your favourite colour?
1) Blue
2) Red
3) Green
4) Grey
5) Magenta
> Aquamarine
Invalid input. Please choose an option from the menu.
> 8
Invalid input. Please choose an option from the menu.
> 1
What is your favourite toy?
> Teddie bear
Your name is Samantha.
You are from Dublin.
Your favourite colour is Blue.
Your favourite toy is Teddie bear.
And there you have it.

Bash IF not working as expected

I am trying to have a function, called from PS1 which outputs something in a different colour, depending on what that something is.
In this case it's $?, the exit status of a program.
I am trying to get this function to output red text if the exit status is anything other than 0.
I have tried all possible variations of this, ways of representing that variable in the conditions and so forth and it just isn't working.
Instead of outputting what I expect it's just either always $LRED in one variation of this IF, or always $HII in another variation of this IF.
All relevant BASH is posted below, can you guys offer any insight?
...
# Custom Colour Alias
NM="\[\033[0;38m\]" # No background and white lines
HI="\[\033[1;36m\]" # Username colour
HII="\[\033[0;37m\]" # Name colour
SI="\[\033[1;32m\]" # Directory colour
IN="\[\033[0m\]" # Command input color
LRED="\[\033[1;31m\]"
BRW="\[\033[0;33m\]"
...
exitStatus ()
{
if [ $? -ne 0 ]
then
echo "$LRED\$?"
else
echo "\$?"
fi
#echo \$?
}
...
export PS1="\n$HII[ $LRED\u $SI\w$NM $HII]\n[ \! / \# / $(exitStatus) $HII]$LRED $ $IN"
CODE BASED ON SOLUTION
This is what I did based on the accepted answer below.
# Before Prompt
export PROMPT_COMMAND='EXSO=$?;\
if [[ $EXSO != 0 ]];\
then\
ERRMSG="$LRED$EXSO";\
else\
ERRMSG="$EXSO";\
fi;\
PS1="\n$HII[ $LRED\u $SI\W$NM $HII\! / \# / $ERRMSG $HII] $SI$ $IN";'
Problem is that your assignment to PS1 is only evaluated once, thus exitStatus is only called once. As Nirk also mentions you should use PROMPT_COMMAND. Set it to the command you want executed before every new prompt is displayed. An example:
PROMPT_COMMAND='if [ $? -ne 0 ]; then echo -n FAIL:;fi'
Will yell FAIL: before every new prompt if the previous command failed:
mogul#linuxine:~$ date
Sun Sep 29 21:13:53 CEST 2013
mogul#linuxine:~$ rm crappy_on_existent_file
rm: cannot remove ‘crappy_on_existent_file’: No such file or directory
FAIL:mogul#linuxine:~$

BASH scripts : whiptail file select

I've found a great little program that will allow me to add user friendly GUI's to my Bash Scripts;
whiptail
However the whiptail man page isn't all that helpful and doesn't provide any examples. After doing some google searches I understand how to create a simple yes/no menu using whiptail:
#! /bin/bash
# http://archives.seul.org/seul/project/Feb-1998/msg00069.html
if (whiptail --title "PPP Configuration" --backtitle "Welcome to SEUL" --yesno "
Do you want to configure your PPP connection?" 10 40 )
then
echo -e "\nWell, you better get busy!\n"
elif (whiptail --title "PPP Configuration" --backtitle "Welcome to
SEUL" --yesno " Are you sure?" 7 40)
then
echo -e "\nGood, because I can't do that yet!\n"
else
echo -e "\nToo bad, I can't do that yet\n"
fi
But what I would really like to build a file select menu using whiptail to replace some old code I have in a few different backup/restore bash scripts I have:
#!/bin/bash
#This script allows you to select a file ending in the .tgz extension (in the current directory)
echo "Please Select the RESTORE FILE you would like to restore: "
select RESTOREFILE in *.tgz; do
break #Nothing
done
echo "The Restore File you selected was: ${RESTOREFILE}"
I assume this has to be done via the '--menu' option of whiptail, but I am not sure how to go about it?
Any pointers?
Or can you point me in the direction of some whiptail examples?
Build an array of file names and menu select tags:
i=0
s=65 # decimal ASCII "A"
for f in *.tgz
do
# convert to octal then ASCII character for selection tag
files[i]=$(echo -en "\0$(( $s / 64 * 100 + $s % 64 / 8 * 10 + $s % 8 ))")
files[i+1]="$f" # save file name
((i+=2))
((s++))
done
A method like this will work even if there are filenames with spaces. If the number of files is large, you may have to devise another tag strategy.
Using alpha characters for the tags lets you press a letter to jump to the item. Numeric tags don't seem to do that. If you don't need that behavior, then you can eliminate some complexity.
Display the menu:
whiptail --backtitle "Welcome to SEUL" --title "Restore Files" \
--menu "Please select the file to restore" 14 40 6 "${files[#]}"
If the exit code is 255, the dialog was canceled.
if [[ $? == 255 ]]
then
do cancel stuff
fi
To catch the selection in a variable, use this structure (substitute your whiptail command for "whiptail-command"):
result=$(whiptail-command 2>&1 >/dev/tty)
Or
result=$(whiptail-command 3>&2 2>&1 1>&3-)
The variable $result will contain a letter of the alphabet that corresponds to a file in the array. Unfortunately, Bash prior to version 4 doesn't support associative arrays. You can calculate the index into the array of the file from the letter like this (notice the "extra" single quote):
((index = 2 * ( $( printf "%d" "'$result" ) - 65 ) + 1 ))
Example:
Welcome to SEUL
┌──────────┤ Restore Files ├───────────┐
│ Please select the file to restore │
│ │
│ A one.tgz ↑ │
│ B two.tgz ▮ │
│ C three.tgz ▒ │
│ D another.tgz ▒ │
│ E more.tgz ▒ │
│ F sp ac es.tgz ↓ │
│ │
│ │
│ <Ok> <Cancel> │
│ │
└──────────────────────────────────────┘
Whiptail is a lightweight reimplementation of the most popular features of dialog, using the Newt library. I did a quick check, and many features in Whiptail seem to behave like their counterparts in dialog. So, a dialog tutorial should get you started. You can find one here but Google is your friend of course. On the other hand, the extended example probably contains a lot of inspiration for your problem.
This function is part of my function repository for whiptail
# ----------------------------------------------------------------------
# File selection dialog
#
# Arguments
# 1 Dialog title
# 2 Source path to list files and directories
# 3 File mask (by default *)
# 4 "yes" to allow go back in the file system.
#
# Returns
# 0 if a file was selected and loads the FILE_SELECTED variable
# with the selected file.
# 1 if the user cancels.
# ----------------------------------------------------------------------
function dr_file_select
{
local TITLE=${1:-$MSG_INFO_TITLE}
local LOCAL_PATH=${2:-$(pwd)}
local FILE_MASK=${3:-"*"}
local ALLOW_BACK=${4:-no}
local FILES=()
[ "$ALLOW_BACK" != "no" ] && FILES+=(".." "..")
# First add folders
for DIR in $(find $LOCAL_PATH -maxdepth 1 -mindepth 1 -type d -printf "%f " 2> /dev/null)
do
FILES+=($DIR "folder")
done
# Then add the files
for FILE in $(find $LOCAL_PATH -maxdepth 1 -type f -name "$FILE_MASK" -printf "%f %s " 2> /dev/null)
do
FILES+=($FILE)
done
while true
do
FILE_SELECTED=$(whiptail --clear --backtitle "$BACK_TITLE" --title "$TITLE" --menu "$LOCAL_PATH" 38 80 30 ${FILES[#]} 3>&1 1>&2 2>&3)
if [ -z "$FILE_SELECTED" ]; then
return 1
else
if [ "$FILE_SELECTED" = ".." ] && [ "$ALLOW_BACK" != "no" ]; then
return 0
elif [ -d "$LOCAL_PATH/$FILE_SELECTED" ] ; then
if dr_file_select "$TITLE" "$LOCAL_PATH/$FILE_SELECTED" "$FILE_MASK" "yes" ; then
if [ "$FILE_SELECTED" != ".." ]; then
return 0
fi
else
return 1
fi
elif [ -f "$LOCAL_PATH/$FILE_SELECTED" ] ; then
FILE_SELECTED="$LOCAL_PATH/$FILE_SELECTED"
return 0
fi
fi
done
}
The use is simple
if dr_file_select "Please, select a file" /home/user ; then
echo "File Selected: \"$FILE_SELECTED\"."
else
echo "Cancelled!"
fi
I've tried following, which worked:
whiptail --title "PPP Config" --backtitle "Welcome to SEUL" --menu YourTitle 20 80 10 `for x in $(ls -1 *.tgz); do echo $x "-"; done`
you might change this into a multiple-liner as well, i've added checking for empty list:
MYLIST=`for x in $(ls -1 *.tgz); do echo $x "-"; done`
WC=`echo $MYLIST | wc -l`
if [[WC -ne 0]]; then
whiptail --title "PPP Config" --backtitle "Welcome to SEUL" --menu YourTitle 20 80 10 $MYLIST
fi
you need to adjust the numbers in order to get a cleaninterface. And you may replace the "-" by anything else if you want to. But if you don't, you will see 2 entries per line.
By the way: The selected entry is printed onto stderr.
This could need some more improving, but for a basic idea I think it's enough.
This seems to be one of the top results when you search for whiptail, and none of the previous results worked for me. This is what I wound up using:
#! /bin/bash
shopt -s nullglob
dir=`pwd`
cd /path/to/files
arr=(*.tgz)
for ((i=0; i<${#arr[#]}; i++)); do j=$((2*$i+2)); a[j]="${arr[$i]}"; a[j+1]=""; done
a[0]=""
# Next line has extra spaces at right to try to center it:
a[1]="Please make a selection from the files below "
result=$(whiptail --ok-button "OK button text" --cancel-button "Cancel Button Text" --title "Title Text" --backtitle "Text at upper left corner of page" --menu "Menu Text (not used??)" 30 160 24 "${a[#]}" 2>&1 >/dev/tty)
if [[ $? = 0 ]]
then
# ge 5 in next line should be length of file extension including . character, plus 1
[ ${#result} -ge 5 ] && outfile="/path/to/files/$result" || echo "Selection not made"
fi
cd "$dir"
$result will be empty if no valid selection was made. I added a dummy selection at the top of the list that returns an empty string as a result, so that you won't accidentally select the wrong file by accidentally hitting Enter right after the menu comes up. If you don't want that, then in the "for" line remove the +2 in "do j=$((2*$i+2))" and also the following two lines that set a[0] and a[1] explicitly.
The confusing thing about whiptail is that when reading from an array in a situation like this it expects two data items per line, both of which are displayed, the first being the result you want returned if the line is expected (which in some situations might be a letter or a number) and the second being whatever descriptive text you may want. That's why for the first line I use a[0] to give an empty string as the result, and a[1] as the descriptive text, but from there on the first item in the pair contains the filename (which is what I actually want returned) and the second is an empty string, since I don't want to display any text other than the filename on those lines.
Also a previous post said whiptail returned an error code of 255 if the cancel button was pressed, but that was not the case for the version I have - it returns 1. So I just test for an error code of 0 and if it is I assume it may be a valid entry, then I test for a valid string length (more than just the number of characters in the file extension, including the . character) to be sure.

Resources