9

I am making a tool script for my theme with 2 functions: Check for update, reinstall theme

So here is the code for selection menu:

PS3='Choose an option: '
options=("Check for update" "Reinstall theme")
select opt in "${options[@]}"
do
     case $opt in
             "Check for update")
                       echo "Checking update"
                               ;;
             "Reinstall theme")
                       echo "Reinstalling"
                               ;;
                               *) echo invalid option;;
     esac
done

When running it appear like this

1) Check for update
2) Reinstall theme
Choose an option:

I type 1 and enter, the check for update command is performed

The problem is when it finished performing the script, it re-display "Choose an option:" not with the menu. So it can make users hard to choose without the menu (especially after a long script)

1) Check for update
2) Reinstall theme
Choose an option: 1
Checking update
Choose an option:

So how can I re-display the menu after an option is performed

Rui F Ribeiro
  • 55,929
  • 26
  • 146
  • 227
superquanganh
  • 3,721
  • 5
  • 14
  • 14

5 Answers5

11

I'm guessing you really want something like this:

check_update () {
    echo "Checking update"
}

reinstall_theme () {
    echo "Reinstalling theme"
}

while true; do
    options=("Check for update" "Reinstall theme")

    echo "Choose an option:"
    select opt in "${options[@]}"; do
        case $REPLY in
            1) check_update; break ;;
            2) reinstall_theme; break ;;
            *) echo "What's that?" >&2
        esac
    done

    echo "Doing other things..."

    echo "Are we done?"
    select opt in "Yes" "No"; do
        case $REPLY in
            1) break 2 ;;
            2) break ;;
            *) echo "Look, it's a simple question..." >&2
        esac
    done
done

I've separated out the tasks into separate function to keep the first case statement smaller. I've also used $REPLY rather than the option string in the case statements since this is shorter and won't break if you decide to change them but forget to update them in both places. I'm also choosing to not touch PS3 as that may affect later select calls in the script. If I wanted a different prompt, I would set it once in and leave it (maybe PS3="Your choice: "). This would give a script with multiple questions a more uniform feel.

I've added an outer loop that iterates over everything until the user is done. You need this loop to re-display the question in the first select statement.

I've added break to the case statements, otherwise there's no way to exit other than interrupting the script.

The purpose of a select is to get an answer to one question from the user, not really to be the main event-loop of a script (by itself). In general, a select-case should really only set a variable or call a function and then carry on.

A shorter version that incorporates a "Quit" option in the first select:

check_update () {
    echo "Checking update"
}

reinstall_theme () {
    echo "Reinstalling theme"
}

while true; do
    options=("Check for update" "Reinstall theme" "Quit")

    echo "Choose an option: "
    select opt in "${options[@]}"; do
        case $REPLY in
            1) check_update; break ;;
            2) reinstall_theme; break ;;
            3) break 2 ;;
            *) echo "What's that?" >&2
        esac
    done
done

echo "Bye bye!"
Kusalananda
  • 320,670
  • 36
  • 633
  • 936
  • Using an outside infinite loop with break 2 is a good clean solution to menu redrawing. Putting the options assignment outside that loop and using PS3 is a small bit cleaner – Rondo Jan 01 '22 at 06:12
  • Resetting REPLY to an empty value after the `esac` will have the desired effect and is much cleaner. See @kanlukasz answer. – Gerard van Helden Apr 12 '23 at 13:10
  • @GerardvanHelden That depends on what the user means by "after a long script". I don't really see how their answer is any "cleaner" if a long sequence of commands is to be fitted into it. Also, as I wrote, the purpose of the `select` loop is to get a selection from the user, not to be the main event loop of the script. – Kusalananda Apr 12 '23 at 13:32
  • It's debatable of course, but if it acts like a loop, why not use it as a loop? – Gerard van Helden Apr 12 '23 at 19:43
3

Your do loop is endless and your select statement is outside of it. The script executes the select statement once and then stays in the do loop checking for case $opt over and over. My recommendation would be to put break after your case statement like this:

esac
break
done

Then, if you really want the whole script to repeat itself over and over again, create another loop that encloses everything from the select statement to the done statement.

bashBedlam
  • 999
  • 6
  • 6
3

I use this trick:

options=("First option" "Second option" "Quit")
PS3="So what? "
select opt in "${options[@]}"
do
    case $opt in
        "First option")
            echo "First option"
            ;;
        "Second option")
            echo "Second option"    
            ;;
        "Quit")
            echo "We are done..."
            break
            ;;
        *) 
            PS3="" # this hides the prompt
            echo asdf | select foo in "${options[@]}"; do break; done # dummy select 
            PS3="So what? " # this displays the common prompt
            ;;
    esac
done
xerostomus
  • 339
  • 3
  • 6
1

You can achieve this by set the REPLY variable to be empty after esac.

Example with your code:

#!/usr/bin/env bash

PS3="Choose an option: "
options=("Check for update" "Reinstall theme")
select opt in "${options[@]}" ; do
    case $opt in
        "Check for update")
            echo "Checking update"
        ;;
        "Reinstall theme")
            echo "Reinstalling"
        ;;
        *)
            echo "invalid option"
        ;;
    esac
    REPLY=
done

Another example ( simpler version to understand ):

#!/usr/bin/env bash

select animal in dog duck cat bird quit ; do
    case $animal in
        dog)  echo "i'm a dog!" ;;
        duck) echo "i'm a duck!" ;;
        cat)  echo "i'm a cat!" ;;
        bird) echo "i'm a bird!" ;;
        quit) echo "bye" ; break ;;
        *)    echo "unknown animal, please try again..." ;;
    esac
    REPLY=
done

For an explanation, see the GNU bash documentation about conditional constructs - select

kanlukasz
  • 111
  • 3
0

You can also check my method, I handled it via a simple function and a for loop

for((i=0;i<${#opt[@]};i++));do echo "$((i+1))) ${opt[$i]}";done

and this is my sample code :

msg="Choose an option:"
opt=("Check_for_update" "Reinstall_theme")

refresh_the_list(){
echo -e "\n$msg"
for((i=0;i<${#opt[@]};i++))
do
        echo "$((i+1))) ${opt[$i]}"
done
}
echo $msg
select i in ${opt[@]}
do
        case $i in
                        ${opt[0]})
                                echo "Checking update"
                                refresh_the_list;;
                        ${opt[1]})
                                echo "Reinstalling"
                                refresh_the_list;;
                        *)
                                echo "invalid option"
                                refresh_the_list;;
        esac
done
Freeman
  • 261
  • 2
  • 3
  • 10