Menu question

Discussion forum for all Windows batch related topics.

Moderator: DosItHelp

Post Reply
Message
Author
SIMMS7400
Posts: 475
Joined: 07 Jan 2016 07:47

Menu question

#1 Post by SIMMS7400 » 26 Aug 2016 06:31

HI Folks -

TO preface this, I'm a stickler for keeping code neat, clean and easy to read. Therefore, in an effort to use as few GOTO labels as possible, I'm wondering if I can do the following:

I have a few scripts that users execute in order to perform various SQL actions. A lot of the pertinent variables are entered by the user themselves. There are sections there users can also choice from a list of actions.

During this stage (when the user needs to chose) there are times when a user presses and errant key or fat fingers something, causing the script to fail.

Therefore, I've put in the following piece of code to return back to to the menu prompt if a choice not in the list is chosen:

Code: Select all

@ECHO OFF
:: DOS TIPS EXAMPLE


:1
echo --------------------------------------
echo [1] Exit - No further action required
echo [2] Map Ghost Users
echo [3] Redo Option: %INT_OPT%
echo.
set /p BRSQLDB_Q=Option:

IF %BRSQLDB_Q%==1 GOTO S4
IF %BRSQLDB_Q%==2 GOTO S5
IF %BRSQLDB_Q%==3 GOTO GET_SQL_CREDS
for %%i in (1 2 3) do if %BRSQLDB_Q% NEQ %%i ( GOTO 1 )

:S4
:S5
:GET_SQL_CREDS


So my question is, (and it may bbe rather silly) is there anyway to return to the menu prompts WITHOUT using a GOTO label?
I ask because scripts can become cluttered with many GOTO labels and look "bad". I know they are necessary but figured I'd ask.

In peusdo code:

Code: Select all

@ECHO OFF
:: DOS TIPS EXAMPLE


echo --------------------------------------
echo [1] Exit - No further action required
echo [2] Map Ghost Users
echo [3] Redo Option: %INT_OPT%
echo.
set /p BRSQLDB_Q=Option:

IF %BRSQLDB_Q%==1 GOTO S4
IF %BRSQLDB_Q%==2 GOTO S5
IF %BRSQLDB_Q%==3 GOTO GET_SQL_CREDS
for %%i in (1 2 3) do if %BRSQLDB_Q% NEQ %%i ( GOTO "Clear last set variable and reset again with choice")

:S4
:S5
:GET_SQL_CREDS


Or something like that.

Thanks!

Squashman
Expert
Posts: 4195
Joined: 23 Dec 2011 13:59

Re: Menu question

#2 Post by Squashman » 26 Aug 2016 07:57

Why not use the CHOICE command instead? That way they cannot enter in an invalid option.

SIMMS7400
Posts: 475
Joined: 07 Jan 2016 07:47

Re: Menu question

#3 Post by SIMMS7400 » 26 Aug 2016 11:47

SO simple lol

This is a great idea. Thank you. I've adjusted my script accordingly.

Thanks!

douglas.swehla
Posts: 75
Joined: 01 Jun 2016 09:25

Re: Menu question

#4 Post by douglas.swehla » 26 Aug 2016 15:25

SIMMS7400 wrote:is there anyway to return to the menu prompts WITHOUT using a GOTO label?
I ask because scripts can become cluttered with many GOTO labels and look "bad". I know they are necessary but figured I'd ask.


First, I agree with Squashman, and I'm glad to see you took his advice. For the benefit of anyone who is not familiar with how to use CHOICE, here are a couple variations on how to accomplish the current task. The first is more generic, and the second uses the options from your example script.

Code: Select all

:: choice_menu1.bat
@echo off
setlocal

rem :: Present options to user. This can be done in the CHOICE command itself.
echo [1] Option 1
echo [2] Option 2
echo [3] Option 3
echo\

rem :: Get input from user. (Message always has space added after it.)
rem :: The /N switch prevents showing the [123] options, since we did that above.
choice /c 123 /n /m "Enter a number matching one of the options above:"

rem :: Process input. Must test ERRORLEVEL values in descending order.
rem :: IF ERRORLEVEL is same as IF %ERRORLEVEL% GEQ (greater than or equal to).
rem :: ERRORLEVEL 0 means user used CTRL+BREAK or CTRL+C to not enter input.
rem :: ERRORLEVEL 255 means an error in the CHOICE command itself.
if not errorlevel 255 if errorlevel 1 (
   rem :: You can use GOTO or CALL here.
   if errorlevel 3 goto sub_3
   if errorlevel 2 goto sub_2
   if errorlevel 1 goto sub_1
) else echo There was an error.

endlocal
exit /b

::=============================================================================
:: Subroutines
::=============================================================================
:sub_1
   echo You chose option 1.
goto :eof

:sub_2
   echo You chose option 2.
goto :eof

:sub_3
   echo You chose option 3.
goto :eof


Code: Select all

:: choice_menu2.bat
@echo off
setlocal

rem :: Manually set value for demo.
set "INT_OPT=5"

rem :: Get input from user. (Message always has space added after it.)
choice /c EMR /m "Press E to Exit, M to Map Ghost Users, or R to Redo Option: %INT_OPT%:"

rem :: Process input. Must test ERRORLEVEL values in descending order.
rem :: IF ERRORLEVEL is same as IF %ERRORLEVEL% GEQ (greater than or equal to).
rem :: ERRORLEVEL 0 means user used CTRL+BREAK or CTRL+C to not enter input.
rem :: ERRORLEVEL 255 means an error in the CHOICE command itself.
if not errorlevel 255 if errorlevel 1 (
   rem :: You can use GOTO or CALL here.
   if errorlevel 3 call :redo_option %INT_OPT%
   if errorlevel 2 call :map_ghost_users
   if errorlevel 1 goto :exit
) else echo There was an error.

:exit
endlocal
exit /b

::=============================================================================
:: Subroutines
::=============================================================================
:redo_option
   echo You chose to redo option %1.
goto :eof

:map_ghost_users
   echo You chose to map ghost users.
goto :eof


Second, GOTOs really aren't evil. While CHOICE is the ideal tool for what you're doing, not every system has it installed. It was standard for a long time, then was taken away on 2000 and XP, then put back on Vista and later. Sometimes we have to accomodate XP users, which is why the SET /P approach is so popular.

All the different kinds of FOR/WHILE/UNTIL loops you see in various languages are just fancy wrappers around a sequence of "test condition then goto line" commands. Since BATCH doesn't include a WHILE command, it's perfectly reasonable to roll your own. Your current code can be simplified so that the GOTO is a little cleaner:

Code: Select all

@ECHO OFF
:: DOS TIPS EXAMPLE

:1
echo --------------------------------------
echo [1] Exit - No further action required
echo [2] Map Ghost Users
echo [3] Redo Option: %INT_OPT%
echo.
set /p BRSQLDB_Q=Option:

IF %BRSQLDB_Q%==1 GOTO S4
IF %BRSQLDB_Q%==2 GOTO S5
IF %BRSQLDB_Q%==3 GOTO GET_SQL_CREDS
rem :: for %%i in (1 2 3) do if %BRSQLDB_Q% NEQ %%i ( GOTO 1 )
rem :: Getting this far means that all the IF tests failed, so entry must be invalid.
GOTO 1

:S4
:S5
:GET_SQL_CREDS


Lastly, yes, it can be done, but at a cost. The only way I know is to create an infinite loop, and inside the loop, prompt for input. If the input is good, break out of the loop. If it's not, echo an error message, and the loop will automatically take you back to the top and prompt for input again. The catch is that breaking out of the loop requires quitting the batch entirely. On the plus side, it runs a bit faster than the GOTO version, since the loop loads all commands into memory, while the GOTO requires repeated file scans.

Code: Select all

:: menu_loop.bat
@echo off
setlocal enableDelayedExpansion

rem :: Create an infinite loop instead of defining a label.
rem :: The counter increments by 0, so will never reach 2.
for /l %%L in (1,0,2) do (
   
   rem :: Present options to user.
   echo [1] Option 1
   echo [2] Option 2
   echo [3] Option 3
   echo\
   
   rem :: Get input from user.
   set /p "choice=Enter choice number: "
   
   rem :: If valid option, call appropriate subroutine, then break out of loop.
   rem :: Breaking out requires quitting the batch; I haven't found a way to
   rem :: continue on to other tasks. It might be possible by calling external
   rem :: batch files instead of subroutines. Haven't tried that.
   rem :: Breakout technique courtesy of our very own jeb, via StackOverflow.
   rem :: http://stackoverflow.com/a/5488111/3326340
   for %%A in (1 2 3) do if "%%A" equ "!choice!" (
      call :sub_!choice!
      call :stop
   )
   
   rem :: If we got past the inner FOR loop, we must have an invalid option.
   echo Invalid option. Try again.
)

rem :: The call to :stop forces the batch to quit, so we never reach the EXIT.
rem :: I include it anyway because not having an EXIT makes me nervous.
exit /b

::=============================================================================
:: Subroutines
::=============================================================================
:sub_1
   echo You chose option 1.
goto :eof

:sub_2
   echo You chose option 2.
goto :eof

:sub_3
   echo You chose option 3.
goto :eof


:stop
call :__stop 2>nul

:__stop
() creates a syntax error, quits the batch

Aacini
Expert
Posts: 1669
Joined: 06 Dec 2011 22:15
Location: México City, México

Re: Menu question

#5 Post by Aacini » 26 Aug 2016 16:31

I would like to add a couple comments about the methods described by douglas.swehla.

If the CHOICE command return a series of known values, then there is not need to check for each one of them; it is simpler to just use the value:

Code: Select all

@echo off
setlocal

rem :: Present options to user.
echo [1] Option 1
echo [2] Option 2
echo [3] Option 3
echo/

rem :: Get input from user. (Message always has space added after it.)
rem :: The /N switch prevents showing the [123] options, since we did that above.
choice /c 123 /n /m "Enter a number matching one of the options above:"

rem :: The errorlevel returned by CHOICE is 1, 2 or 3; so just use it directly:
call :sub_%errorlevel%
exit /b

::=============================================================================
:: Subroutines
::=============================================================================
:sub_1
   echo You chose option 1.
goto :eof

:sub_2
   echo You chose option 2.
goto :eof

:sub_3
   echo You chose option 3.
goto :eof

When an infinite loop is break, the Batch file not necessarily must end. There is a way to start an infinite loop, break it via an option and continue with the rest of the Batch file:

Code: Select all

@echo off
setlocal EnableDelayedExpansion
if "%1" equ "Menu" goto Menu

echo Before the menu
rem :: Start the infinite loop
cmd /C "%~F0" Menu
echo/
echo After the menu
goto :EOF

:Menu
rem :: Create an infinite loop instead of defining a label.
for /L %%L in () do (

   rem :: Present options to user.
   echo/
   echo/
   echo [1] Option 1
   echo [2] Option 2
   echo [3] Option 3
   echo [x] Exit menu
   echo/

   rem :: Get input from user. (Message always has space added after it.)
   rem :: The /N switch prevents showing the [123] options, since we did that above.
   choice /c 123X /n /m "Enter a number matching one of the options above:"

   rem :: The errorlevel returned by CHOICE is 1, 2, 3 or 4; so just use it directly:
   call :sub_!errorlevel!
)



::=============================================================================
:: Subroutines
::=============================================================================
:sub_1
   echo You chose option 1.
goto :eof

:sub_2
   echo You chose option 2.
goto :eof

:sub_3
   echo You chose option 3.
goto :eof

:sub_4
   rem :: Break the infinite loop
exit

Antonio

SIMMS7400
Posts: 475
Joined: 07 Jan 2016 07:47

Re: Menu question

#6 Post by SIMMS7400 » 29 Aug 2016 01:36

THank you both! I"m going to tinker around this these suggestions today! Thanks!

Post Reply