How to use goto statement in shell script [duplicate] - bash

This question already has answers here:
Is there a "goto" statement in bash?
(14 answers)
Closed 4 years ago.
I am beginner in shell script. I don't have any idea about how to use goto statement. I am using the following code.
start:
echo "Main Menu"
echo "1 for Copy"
echo "2 for exit"
read NUM
case $NUM in
"1")
echo "CopyNUM"
goto start:;
"2")
echo "Haiiii";
goto start:
*)
echo "ssss";
esac

As others have noted, there's no goto in bash (or other POSIX-like shells) - other, more flexible flow-control constructs take its place.
Look for heading Compound Commands in man bash.
In your case, the select command is the right choice.
Since how to use it may not be obvious, here's something to get you started:
#!/usr/bin/env bash
echo "Main Menu"
# Define the choices to present to the user, which will be
# presented line by line, prefixed by a sequential number
# (E.g., '1) copy', ...)
choices=( 'copy' 'exit' )
# Present the choices.
# The user chooses by entering the *number* before the desired choice.
select choice in "${choices[#]}"; do
# If an invalid number was chosen, $choice will be empty.
# Report an error and prompt again.
[[ -n $choice ]] || { echo "Invalid choice." >&2; continue; }
# Examine the choice.
# Note that it is the choice string itself, not its number
# that is reported in $choice.
case $choice in
copy)
echo "Copying..."
# Set flag here, or call function, ...
;;
exit)
echo "Exiting. "
exit 0
esac
# Getting here means that a valid choice was made,
# so break out of the select statement and continue below,
# if desired.
# Note that without an explicit break (or exit) statement,
# bash will continue to prompt.
break
done

Here is a short example using a select loop to accomplish your goal. You can use a while loop with a custom menu if you want custom formatting, but the basic menu is what select was designed to do:
#!/bin/bash
## array of menu entries
entries=( "for Copy"
"for exit" )
## set prompt for select menu
PS3='Selection: '
while [ "$menu" != 1 ]; do ## outer loop redraws menu each time
printf "\nMain Menu:\n\n" ## heading for menu
select choice in "${entries[#]}"; do ## select displays choices in array
case "$choice" in ## case responds to choice
"for Copy" )
echo "CopyNUM"
break ## break returns control to outer loop
;;
"for exit" )
echo "Haiiii, exiting"
menu=1 ## variable setting exit condition
break
;;
* )
echo "ssss"
break
;;
esac
done
done
exit 0
Use/Output
$ bash select_menu.sh
Main Menu:
1) for Copy
2) for exit
Selection: 1
CopyNUM
Main Menu:
1) for Copy
2) for exit
Selection: 3
ssss
Main Menu:
1) for Copy
2) for exit
Selection: 2
Haiiii, exiting

Related

Simplify Dynamic Menu in Bash

I'm working on a custom bash script (with which I have very little experience) and I'm looking for some help with a menu function. My script had several different menus accomplishing different functions so I decided to create one menu function that could be called and customized with set variables. It's unclean and ugly, but I've learned a lot making it. Can anyone shed some light on where I could have simplified or done things differently or more cleanly? Thanks in advance!
#!/bin/bash
# set colors
red='tput setaf 1'
blue='tput setaf 4'
green='tput setaf 2'
bold='tput bold'
normal='tput sgr0'
# define function to reset screen to title
function reset {
clear
$bold
$blue
echo -e "lineage build script\n\n"
$normal
}
function title {
reset
$bold
echo -e "$1\n"
$normal
}
# function to create menu
# menu "<title(required)>" "<question(required)>" <all(required)> "<option1>"...
# <title> - Page title
# <question> - Question to be posed before the menu
# <all> - Whether or not to present a menu choice for "All"
# <option#> - List of options to present to the user
function menu {
# set input at 255 (max error level for return function later)
input=255
# check input to see if it's acceptable (within presented range and a number)
while [[ $input -ge $(($counter+1)) ]] || [[ $((input)) != $input ]]; do
# a call to a previously defind function establishing consistent page feel.
title "$1"
echo -e "$2"
# present warning if input was invalid
if ! [[ $input =~ "255" ]]; then
$red
echo -e "Invalid entry. Please try again.\n"
$normal
else
echo -e "\n"
fi
counter=0
# present list items for each variable passed to function starting at position 4
# in order to skip the first 3 (title, question, all)
for i in "${#:4}"; do
counter=$(($counter+1))
echo -e "$counter) $i"
done
# present "All" option if variable 3 is any form of yes
if [[ $3 =~ (y|yes|Y|YES) ]]; then $green; counter=$(($counter+1)); echo -e "$counter) All"; $normal; fi
# present "Exit" option
$red; counter=$(($counter+1)); echo -e "$counter) Exit\n"; $normal
# gather input
read -N 1 -p "[1-$counter]: " input
done
# bail if exit was chosen
if [[ $input == $counter ]]; then $red; echo -e "\nExit"; exit; fi
# pass choice back to script as return code
return $input
}
# call the menu function for testing
menu "Main Menu" "What would you like to do?" y "option 1" "option 2" "option 3" "option 4" "option 5"
#echo return code just to verify function
echo -e "\nYou chose $?"
Let's see if this helps in anyway (in retro). Not only shell scripting related.
Naming
The reset function is a verb, where as title is not. But it could be, indicating what it does and that it's not a variable.
Printing
You consistently used echo -e for printing. As Charles Duffy pointed out in a comment below, you are sacrificing POSIX compatibility by using -e.
It is not possible to use echo portably across all POSIX systems unless both -n (as the first argument) and escape sequences are omitted.
The printf utility can be used portably to emulate any of the traditional behaviors of the echo utility as follows (assuming that IFS has its standard value or is unset)
http://pubs.opengroup.org/onlinepubs/009696799/utilities/echo.html
Whatever actual way you will use for printing: how about making that a function? If you have to change, it's one place to go. Hopefully this won't lead you to a big list of overloaded functions (for the various printing options).
Local
One reason I liked your program is that you kept variables where they belong, locally to the functions. Maybe you could step that up by actually denoting (where it makes sense) them local.
local [option] name[=value]
https://www.gnu.org/software/bash/manual/html_node/Bash-Builtins.html
Shift
To continue the previous point I usually prefer to store / move the ordered parameters to local variables. For example title_text=$1. In the menu function it would allow you to use shift. You would process "title", "menu name" and "all" options then you end up with only the options list in the $# variable. A list of option names. Very handy if you later want to refactor some parts of the function (without breaking order) or you want to pass said list to another function to deal with the options.
shift [n]
Shift the positional parameters to the left by n. The positional
parameters from n+1 … $# are renamed to $1 … $#-n. Parameters
represented by the numbers $# to $#-n+1 are unset. n must be a
non-negative number less than or equal to $#. If n is zero or greater
than $#, the positional parameters are not changed. If n is not
supplied, it is assumed to be 1. The return status is zero unless n is
greater than $# or less than zero, non-zero otherwise.
https://www.gnu.org/software/bash/manual/html_node/Bourne-Shell-Builtins.html
Exit codes
I have found your way of use for the return values (exit codes) nice. It's somewhat a pity that the only situation where it's not used is the actual case when you call exit. What I mean is that without going into the menu function, I was confused for a little while why I never see "You chose" if I "exit".

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.

Print out a list of all cases of a switch

Curious question. Is it somehow possible to print out all cases of a certain switch-case automatically in bash? In a way, such that it stays as maintainable as possible, meaning that one does not have to add any more code if a new case is added to print out that same case.
For instance, that would be useful if the cases represented commands. A help function could then print out all available commands.
There is no direct way to achieve this, but you can use an array to maintain your choices:
# Define your choices - be sure not to change their order later; only
# *append* new ones.
choices=( foo bar baz )
# Make the array elements the case branches *in order*.
case "$1" in
"${choices[0]}")
echo 'do foo'
;;
"${choices[1]}")
echo 'do bar'
;;
"${choices[2]}")
echo 'do baz'
;;
*)
echo "All choices: ${choices[#]}"
esac
This makes the branches less readable, but it's a manageable solution, if you maintain your array carefully.
Note how the branch conditions are enclosed in "..." so as to prevent the shell from interpreting the values as (glob-like) patterns.
That said, as chepner points out, perhaps you want to define your choices as patterns to match variations of a string:
In that event:
Define the pattern with quotes in the choices array; e.g., choices=( foo bar baz 'h*' )
Reference it unquoted in the case branch; e.g., ${choices[3]})
bash does not give you access to the tokens it parses and does not save case strings (which can be glob expressions as well).
Unfortunately, that means you will not be able to DRY your code in the way you were hoping.
After a while of hacking together several Bash 4 features I've got this one.
#!/bin/bash
set -euo pipefail
# create coprocess with 2 descriptors so we can read and write to them
coproc CAT { cat ; }
# creme de la creme of this solution - use function to both collect and select elements
function element {
echo "$1" >&${CAT[1]}
echo "$1"
}
case "$1" in
$(element A))
echo "Your choice is: A"
;;
$(element B))
echo "Your choice is: B"
;;
*)
echo "Your choice is not present in available options: $1"
# close writing descriptor
exec {CAT[1]}>&-
#read colected options into an array
mapfile -t OPTIONS <&${CAT[0]}
echo "Available options are: [ ${OPTIONS[#]} ]"
;;
esac
Output:
Your choice is not present in available options: C
Available options are: [ A B ]
There are 2 parts for this solution:
coproc - which creates subprocess for reading and writing from subshell
function element which both writes into descriptors of coproc subrocess and returns it's argument so we can use it inside case ... esac
If handling all options should be done outside of case then you can use ;;& feature of Bash 4 case statement which forces checking every statement inside case (usually - i.e. ;; - it stops after first match). This checking is needed so we can collect all options into an array later
There is probably a lot of reasons not to use this (limits of data which can be safely stored in descriptor without reading them being one of those) and I welcome all comments which can make this solution better.
You could have your script inspect itself:
#!/bin/bash
case_start=$(("$LINENO" + 2)) #store line number of start
case "$1" in
simple_case) echo "this is easy";;
--tricky)
echo "This is a tricky"
(echo "Multiline")
echo "Statement"
;;
-h|--help) echo "heeeelp me";;
-q|--quiet) ;;
*) echo "unknown option";;
esac
case_end=$(("$LINENO" - 2)) #store line number of end
# - take lines between $case_start and $case_end
# - replace newlines with spaces
# - replace ";;" with newlines
# -=> now every case statement should be on its own line
# - then filter out cases: delete everything after the first ")" including the ")" and trim blanks
cases_available=`sed -n "${case_start},${case_end}p" $0 | sed 's/#.*//' | tr '\n' ' ' | sed 's/;;/\n/g' | sed 's/).*//;s/[[:blank:]]*//'`
echo -e "cases_available:\n\n$cases_available"
this would print:
cases_available:
simple_case
--tricky
-h|--help
-q|--quiet
*
There are some pitfalls with this:
Comments or strings inside the case statement with a ";;" in it will break stuff
Can't handle nested switch case statements.
Dunno if I understood correctly your question, but you can do something like this page says to print out all the available cases as default choice:
case "$1" in
start)
start
;;
stop)
stop
;;
status)
status anacron
;;
restart)
stop
start
;;
condrestart)
if test "x`pidof anacron`" != x; then
stop
start
fi
;;
*)
echo $"Usage: $0 {start|stop|restart|condrestart|status}"
exit 1
esac
I think the best approach and also in OP regards is to actually print the switch case structure to a new file and then source it in the original file if needed.
echo "case "'"$1"'" in" > case.sh
echo " test)" >> case.sh
echo " echo "This case!"" >> case.sh
echo " ;;" >> case.sh
echo "esac" >> case.sh
chmod+x case.sh
./case.sh test
#This case!
This way you can easily use variables to build your switch / case condition.

Bash case statement restart on invalid input

Trying to figure out a way to return in a case statement. For example, if you run this....
while :
do
clear
cat<<-EOF
======================
Foo Bar Doo Dah Setup
======================
(1) Foo
(2) Bar
(q) Quit
----------------------
EOF
read
case $REPLY in
"1") foo="foo" ;;
"2") bar="bar" ;;
"q") break ;;
* ) echo "Invalid Option"
esac
sleep .5
clear
cat<<-EOF
======================
Foo Bar Doo Dah Setup
======================
(1) Doo
(2) Dah
(q) Quit
----------------------
EOF
read
case $REPLY in
"1") doo="doo" ;;
"2") dah="dah" ;;
"q") break ;;
* ) echo "Invalid Option"
esac
sleep .5
done
...and enter an "Invalid Option" then you'll notice it moves on to the next case instead of re-evaluating the case.
The workaround isn't too bad just have to nest the case statement into an if statement within a while loop ...
while read; do
if [ $REPLY -ge 1 -a $REPLY -le 2 ]; then
case $REPLY in
"1") foo="foo" ;;
"2") bar="bar" ;;
esac
break
elif [ $REPLY == q ]; then
break
else
echo "Invalid Option"
fi
done
That being said seems a bit much, anyone know of some form a loop control to rerun a case statement from a case selection?
If you need a valid choice from the menu before continuing to the next menu, then you need a loop around the choosing process. You should probably also count the failures and terminate after some number of consecutive failures such as 10.
The shell loop constructs support both break and continue, and these can optionally be followed by a number indicating how many loop levels should be broken (the default number of levels is 1).
The code should also heed EOF detected by read and terminate the loops. That's achieved with the answer variable in the code below.
This leads to code like:
retries=0
max_retries=10
while [ $retries -lt $max_retries ]
do
clear
cat <<-'EOF'
======================
Foo Bar Doo Dah Setup
======================
(1) Foo
(2) Bar
(q) Quit
----------------------
EOF
retries=0
answer=no
while read
do
case "$REPLY" in
"1") foo="foo"; answer=yes; break;;
"2") bar="bar"; answer=yes; break;;
"q") break 2;; # Or exit, or return if the code is in a function
* ) echo "Invalid Option ('$REPLY' given)" >&2
if [ $((++retries)) -ge $max_retries ]; then break 2; fi
;;
esac
done
if [ "$answer" = "no" ]; then break; fi # EOF in read loop
sleep .5
clear
cat <<-'EOF'
======================
Foo Bar Doo Dah Setup
======================
(1) Doo
(2) Dah
(q) Quit
----------------------
EOF
retries=0
answer=no
while read
do
case $REPLY in
"1") doo="doo"; answer=yes;;
"2") dah="dah"; answer=yes;;
"q") break 2;;
* ) echo "Invalid Option ('$REPLY' given)"" >&2
if [ $((++retries)) -ge $max_retries ]; then break 2; fi
;;
esac
done
if [ "$answer" = "no" ]; then break; fi # EOF in read loop
sleep .5
echo "$foo$bar $doo$dah" # Do something with the entered information
done
I'm not entirely keen on read with no name after it, not least because it is a Bash extension rather than standard shell functionality (the POSIX read utility does not provide a default variable name), and omitting the name unnecessarily limits the scripts portability.
Note, too, that the here documents have the start marker enclosed in quotes so that the content of the here document is not subjected to shell expansions. The - indicates that leading tabs (but not spaces) are deleted from the here document and the end of document marker line.
The code in the question for the first loop could also be 'fixed' by using a continue instead of a nested while loop. However, if the second loop was to retry just the second prompt, continuing the outer loop and skipping the first menu would be complex. The solution shown is symmetric and properly isolates the input for each of the two prompts.
I chose not to re-echo the menu on a retry, but it would not be hard to loop on that code too. It would be feasible to use:
retries=0
answer=no
while clear
cat <<-'EOF'
======================
Foo Bar Doo Dah Setup
======================
(1) Foo
(2) Bar
(q) Quit
----------------------
EOF
read
do
…processing $REPLY as before…
done
However, doing so will cause many scratched heads as people are not often aware that you can have a list of commands after a while statement and it is only the exit status of the last that controls whether the loop continues another iteration or not.
I personally avoid tabs in shell scripts, so I'd probably create variables to hold the menus:
menu1=$(cat <<-'EOF'
======================
Foo Bar Doo Dah Setup
======================
(1) Foo
(2) Bar
(q) Quit
----------------------
EOF
)
The prompt in the loop could then be:
while clear; echo "$menu1"; read
which is easier to understand (and the double quotes are crucial). You could use && in place of ; if the clear command is well behaved and exits successfully.

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