Safe number comparison in Windows batch file - windows

I know that when comparing stuff for equality in a batch file it's common to enclose both sides in quotes, like
IF "%myvar% NEQ "0"
But when comparing using "greater than" or "less than", this doesn't work because the operands would then be treated as strings with quotes around them. So you can instead just do
IF %myvar% GTR 20000
The caveat is that if the variable %myvar% isn't declared, it would be like doing
IF GTR 20000
which is a syntax error.
I came up with the following workaround:
IF 1%myvar% GTR 120000
which I'm hoping would result in IF 1 GTR 120000 if myvar is undefined, and it seems to work.
Is this a safe way to compare numbers and accounting for undeclared variables, or did I just open up a whole new can of caveats?

Let us assume the batch file contains:
#echo off
:PromptUser
rem Undefine environment variable MyVar in case of being already defined by chance.
set "MyVar="
rem Prompt user for a positive number in range 0 to 20000.
set /P "MyVar=Enter number [0,20000]: "
As I explained by my answer on How to stop Windows command interpreter from quitting batch file execution on an incorrect user input? the user has the freedom to enter really anything including a string which could easily result in breaking batch file execution because of a syntax error or resulting in doing something the batch file is not written for.
1. User entered nothing
If the user hits just key RETURN or ENTER, the environment variable MyVar is not modified at all by command SET. It is easy to verify in this case with environment variable MyVar explicitly undefined before prompting the user if the user entered a string at all with:
if not defined MyVar goto PromptUser
Note: It is possible to use something different than set "MyVar=" like set "MyVar=1000" to define a default value which can be even output on prompt giving the user the possibility to just hit RETURN or ENTER to use the default value.
2. User entered a string with one or more "
The user could enter a string with one or more " intentionally or by mistake. For example pressing on a German keyboard key 2 on non-numeric keyboard with CapsLock currently enabled results in entering ", except German (IBM) is used on which CapsLock is by software only active for the letters. So if the user hits 2 and RETURN quickly or without looking on screen as many people do on typing on keyboard, a double quote character instead of 2 was entered by mistake by the user.
On MyVar holding a string with one or more " all %MyVar% or "%MyVar%" environment variable references are problematic because of %MyVar% is replaced by Windows command processor by user input string with one or more " which nearly always results in a syntax error or the batch file does something it was not designed for. See also How does the Windows Command Interpreter (CMD.EXE) parse scripts?
There are two solutions:
Enable delayed expansion and reference the environment variable using !MyVar! or "!MyVar!" as now the user input string does not affect anymore the command line executed by cmd.exe after parsing it.
Remove all " from user input string if this string should never contain a double quote character.
Character " is definitely invalid in a string which should be a number in range 0 to 20000 (decimal). For that reason two more lines can be used to prevent wrong processing of user input string caused by ".
set "MyVar=%MyVar:"=%"
if not defined MyVar goto PromptUser
The Windows command processor removes all doubles quotes already on parsing this line before replacing %MyVar:"=% with the resulting string. Therefore the finally executed command line set "MyVar=whatever was entered by the user" is safe on execution.
The example above with a by mistake entered " instead of 2 results in execution of set "MyVar=" which undefines the environment variable MyVar which is the reason why the IF condition as used before must be used again before further processing of the user input.
3. User entered non-valid character(s)
The user should enter a positive decimal number in range 0 to 20000. So any other character than 0123456789 in user input string is definitely invalid. Checking for any invalid character can be done for example with:
for /F delims^=0123456789^ eol^= %%I in ("%MyVar%") do goto PromptUser
The command FOR does not execute goto PromptUser if the entire string consists of just digits. In all other cases including a string starting with ; after zero or more digits results in execution of goto PromptUser because of input string contains a non-digit character.
4. User entered number with leading 0
Windows command processor interprets numbers with a leading 0 as octal numbers. But the number should be interpreted as decimal number even on user input it with one or more 0 at beginning. For that reason the leading zero(s) should be removed before further processing variable value.
for /F "tokens=* delims=0" %%I in ("%MyVar%") do set "MyVar=%%I"
if not defined MyVar set "MyVar=0"
FOR removes all 0 at beginning of string assigned to MyVar and assigns to loop variable I the remaining string which is assigned next to environment variable MyVar.
FOR runs in this case set "MyVar=%%I" even on user entered 0 or 000 with the result of executing set "MyVar=" which undefines environment variable MyVar in this special case. But 0 is a valid number and therefore the IF condition is necessary to redefine MyVar with string value 0 on user entered number 0 with one or more zeros.
5. User entered too large number
Now it is safe to use the command IF with operator GTR to validate if the user entered a too large number.
if %MyVar% GTR 20000 goto PromptUser
This last verification works even on user entering 82378488758723872198735897 which is larger than maximum positive 32 bit integer value 2147483647 because of the range overflow results in using 2147483647 on execution of this IF condition. See my answer on weird results with IF for details.
6. Possible solution 1
An entire batch file for safe evaluation of user input number in range 0 to 20000 for only decimal numbers is:
#echo off
set "MinValue=0"
set "MaxValue=20000"
:PromptUser
rem Undefine environment variable MyVar in case of being already defined by chance.
set "MyVar="
rem Prompt user for a positive number in range %MinValue% to %MaxValue%.
set /P "MyVar=Enter number [%MinValue%,%MaxValue%]: "
if not defined MyVar goto PromptUser
set "MyVar=%MyVar:"=%"
if not defined MyVar goto PromptUser
for /F delims^=0123456789^ eol^= %%I in ("%MyVar%") do goto PromptUser
for /F "tokens=* delims=0" %%I in ("%MyVar%") do set "MyVar=%%I"
if not defined MyVar set "MyVar=0"
if %MyVar% GTR %MaxValue% goto PromptUser
rem if %MyVar% LSS %MinValue% goto PromptUser
rem Output value of environment variable MyVar for visual verification.
set MyVar
pause
This solution gives the batch file writer also the possibility to output an error message informing the user why the input string was not accepted by the batch file.
The last IF condition with operator LSS is not needed if MinValue has value 0 which is the reason why it is commented out with command REM for this use case.
7. Possible solution 2
Here is one more safe solution which has the disadvantage that the user cannot enter a decimal number with one or more leading 0 being nevertheless interpreted decimal as expected usually by users.
#echo off
set "MinValue=0"
set "MaxValue=20000"
:PromptUser
rem Undefine environment variable MyVar in case of being already defined by chance.
set "MyVar="
rem Prompt user for a positive number in range %MinValue% to %MaxValue%.
set /P "MyVar=Enter number [%MinValue%,%MaxValue%]: "
if not defined MyVar goto PromptUser
setlocal EnableDelayedExpansion
set /A "Number=MyVar" 2>nul
if not "!Number!" == "!MyVar!" endlocal & goto PromptUser
endlocal
if %MyVar% GTR %MaxValue% goto PromptUser
if %MyVar% LSS %MinValue% goto PromptUser
rem Output value of environment variable MyVar for visual verification.
set MyVar
pause
This solution uses delayed environment variable expansion as written as first option on point 2 above.
An arithmetic expression is used to convert the user input string to a signed 32 bit integer interpreting the string as decimal, octal or hexadecimal number and back to a string assigned to environment variable Number on which decimal numeral system is used by Windows command processor. An error output on evaluation of the arithmetic expression because of an invalid user string is redirected to device NUL to suppress it.
Next is verified with using delayed expansion if the number string created by the arithmetic expression is not identical to the string entered by the user. This IF condition is true on invalid user input including number having leading zeros interpreted octal by cmd.exe or a number entered hexadecimal like 0x14 or 0xe3.
On passing the string comparison it is safe to compare value of MyVar with 20000 and 0 using the operators GTR and LSS.
Please read this answer for details about the commands SETLOCAL and ENDLOCAL because there is much more done on running setlocal EnableDelayedExpansion and endlocal than just enabling and disabling delayed environment variable expansion.
8. Possible solution 3
There is one more solution using less command lines if the value 0 is out of valid range, i.e. the number to enter by the user must be greater 0.
#echo off
set "MinValue=1"
set "MaxValue=20000"
:PromptUser
rem Undefine environment variable MyVar in case of being already defined by chance.
set "MyVar="
rem Prompt user for a positive number in range %MinValue% to %MaxValue%.
set /P "MyVar=Enter number [%MinValue%,%MaxValue%]: "
set /A MyVar+=0
if %MyVar% GTR %MaxValue% goto PromptUser
if %MyVar% LSS %MinValue% goto PromptUser
rem Output value of environment variable MyVar for visual verification.
set MyVar
pause
This code uses set /A MyVar+=0 to convert the user entered string to a 32-bit signed integer value and back to a string as suggested by aschipfl in his comment above.
The value of MyVar is 0 after command line with the arithmetic expression if the user did not input any string at all. It is also 0 if the user input string has as first character not one of these characters -+0123456789 like " or / or (.
A user input string starting with a digit, or - or + and next character is a digit, is converted to an integer value and back to a string value. The entered string can be a decimal number or an octal number or a hexadecimal number. Please take a look on my answer on Symbol equivalent to NEQ, LSS, GTR, etc. in Windows batch files which explains in detail how Windows command processor converts a string to an integer value.
The disadvantage of this code is that a by mistake input string like 7"( instead of 728 caused by holding Shift on pressing the keys 2 and ( on a German keyboard is not detected by this code. MyVar has value 7 on user enters by mistake 7"(. Windows command processor interprets just the characters up to first not valid character for a decimal, hexadecimal or octal number as integer value and ignores the rest of the string.
The batch file using this code is safe against an unwanted exit of batch file processing because of a syntax error never occurs independent on what the user inputs. But a by mistake wrong input number is in some cases not detected by the code resulting in processing the batch file further with a number which the user did not want to use.

Answering the call to nitpick
Mofi has been requesting I write my own solution here, that is "shorter" as I pointed out to him the way he wrote his code using & instead of ( followed by a command then a carriage return and another command, or `( followed by a carriage return, followed by another command followed by a carriage return followed by another command) sets a precedent which makes this a hard task to agree on.
I also did not think this was the POINT of providing the answers perse, I mean I used to, but when changes are minor, and mainly fixing logic, or offering a minorly different solution, is that really a big difference? Does that really warrant being a separate answer?
That said, I don't see a better way without editing his response.. but this still leaves unresolved questions on what is being judged shorter.
Unfortunately as well, in discussing with Mofi he has edited his answer to one that can result in invalid choices.
While I have pointed this out, and I'm sure this was just a minor oversite on his part, I feel like not posting the code here has contributed to him actively deteriorating the quality of his question, which is always a possible outcome when nitpicking.
while Mofi was the driving force in that activity, I don't like the effect it's had on him as I was trying to avoid exactly this effect on my code by not getting into it, so I have decided to post the code comparison to bring some closure for them.
Please not, I will post his original code (the most recent one that did not use the erroneous method), and then refactored to how I would write it, and I will post my Original code, and then refactored to how I believe he would write it (may not be in that order but I will call out each)
So below is the result
Mofi Original:
This is hard to say if you should count every line, there are some instances where & is used to queue up commands and the IFS never use Parenthesis which I wouldn't generally do.
#echo off
set "MinValue=0"
set "MaxValue=20000"
:PromptUser
rem Undefine environment variable MyVar in case of being already defined by chance.
set "MyVar="
rem Prompt user for a positive number in range %MinValue% to %MaxValue%.
set /P "MyVar=Enter number [%MinValue%,%MaxValue%]: "
if not defined MyVar goto PromptUser
setlocal EnableDelayedExpansion
set /A "Number=MyVar" 2>nul
if not "!Number!" == "!MyVar!" endlocal & goto PromptUser
endlocal
if %MyVar% GTR %MaxValue% goto PromptUser
if %MyVar% LSS %MinValue% goto PromptUser
rem Output value of environment variable MyVar for visual verification.
set MyVar
pause
My Code Refactored to Mofi's Form
#ECHO OFF
SETLOCAL EnableDelayedExpansion
SET /A "_Min=-1","_Max=20000"
:Menu
CLS
SET "_Input="
REM Prompt user for a positive number in range %_Min% to %_Max%.
SET /P "_Input=Enter number [%_Min%,%_Max%]: "
SET /A "_Tmp=%_input%" && if /I "!_input!" EQU "!_Tmp!" if !_Input! GEQ %_Min% if !_Input! LEQ %_Max% SET _Input & pause & GOTO :EOF
GOTO :Menu
Mofi's Code Refactored
Mofi's above code Refactored to my more compacted form Where ( have the first command follow except when used on an IF statement, and ) follow the last command. This also makes the entire portion that really does the validation EASY to discern, it is only the portion within the :PromtUser function, not counting REM lines or blank lines this is 13 lines of code.
#(SETLOCAL
echo off
SET /A "MinValue=0","MaxValue=20000")
CALL :Main
( ENDLOCAL
EXIT /B )
:Main
CALL :PromptUser MyVar
REM Output value of environment variable MyVar for visual verIFication.
SET MyVar
PAUSE
GOTO :EOF
:PromptUser
SET "MyVar="
rem Prompt user for a positive number in range %MinValue% to %MaxValue%.
SET /P "MyVar=Enter number [%MinValue%,%MaxValue%]: "
IF NOT DEFINED MyVar GOTO :PromptUser
Setlocal EnableDelayedExpansion
SET /A "Number=MyVar" 2>nul
IF not "!Number!" == "!MyVar!" (
Endlocal
GOTO :PromptUser )
Endlocal
IF %MyVar% GTR %MaxValue% (
GOTO :PromptUser )
IF %MyVar% LSS %MinValue% (
GOTO :PromptUser )
GOTO :EOF
My Code in My Compact Form
To compare here is my code also in the same compact form I refactored Mofi's code to above. Again, only the lines inside of the function itself are "doing the heavy lifting" here and need compare. I did forget that when I worked on my code originally I was trying to match Mofi's form, and it allowed me an extra nicety in keeping my && ( in the following line or all as a single line. So I will post two varients
#(SETLOCAL ENABLEDELAYEDEXPANSION
ECHO OFF
SET /A "_Min=-1","_Max=20000" )
CALL :Main
( ENDLOCAL
EXIT /B )
:Main
CALL :Menu _input
REM Output value of environment variable _input for visual verIFication.
SET _input
PAUSE
GOTO :EOF
:Menu
CLS
SET "_input="
REM Prompt user for a positive number in range %_Min% to %_Max%. Store it in "_input"
SET /P "_Input=Enter number [%_Min%,%_Max%]: "
SET /A "_Tmp=%_input%" && (
IF /I "!_input!" EQU "!_Tmp!" IF !_Input! GEQ %_Min% IF !_Input! LEQ %_Max% GOTO :EOF )
GOTO :Menu
My Code in My Compact Form 2
#(SETLOCAL ENABLEDELAYEDEXPANSION
ECHO OFF
SET /A "_Min=-1","_Max=20000" )
CALL :Main
( ENDLOCAL
EXIT /B )
:Main
CALL :Menu
REM Output value of environment variable _input for visual verification.
SET _input
PAUSE
GOTO :EOF
:Menu
CLS
SET "_input="
REM Prompt user for a positive number in range %_Min% to %_Max%. Store it in "_input"
SET /P "_Input=Enter number [%_Min%,%_Max%]: "
SET /A "_Tmp=%_input%" || GOTO :Menu
IF /I "!_input!" EQU "!_Tmp!" (
IF !_Input! GEQ %_Min% (
IF !_Input! LEQ %_Max% (
GOTO :EOF ) ) )
GOTO :Menu

Related

Batch file seems to be setting a variable to 2 for no reason

I'm making a simple batch script to figure out arrays in batch script.
The code:
#echo off
setlocal EnableDelayedExpansion
set inputCount=0
set outputCount=0
:input
cls
set /p !number%inputCount%!=Input %inputCount%:
set /a inputCount=%inputCount%+1
if %inputCount% geq 3 goto output
goto input
:output
cls
echo !number%outputCount%!
set /a outputCount=%outputCount%+1
if %outputCount% geq 3 goto exit
goto output
:exit
pause
echo exit
On line 4, I set outputCount to 0, I then don't change outputCount until line 16 where I add 1 to it.
I expected the output of line 16 to be outputCount=0+1=1 therefore making outputCount=1. However, when I run the code with echo on to see exactly what it's doing, the output for line 16 is outputCount=2+1=3 setting outputCount to 3.
It seems that the program is setting outputCount to 2 instead of 0 at some point before line 16 but I can't see why.
First, take a look on Debugging a batch file as this is a lesson you need to learn on coding a batch file.
Second, read the answer on Why is no string output with 'echo %var%' after using 'set var = text' on command line? which offers additional information to the help output on running in a command prompt window set /?.
It looks like you want to define the environment variables number0, number1 and number2 with a string assigned to them by user input.
The command to use to prompt a user for a string is either
set /P "variable=prompt text: "
or
set /P variable="prompt text: "
The first variant is in general recommended although most often not used by batch file coding newbies because of not knowing how to use the double quotes right on assigning a string to an environment variable. The second variant is specific for set /P also possible and in some very rare cases really needed, but in my point of view should be avoided to use because of the double quotes are interpreted different on using set without /P.
So let us look on your code with commenting out with rem four lines, appending one more line with set at end of the batch file and run that batch file from within a command prompt window:
rem #echo off
setlocal EnableDelayedExpansion
set inputCount=0
set outputCount=0
:input
rem cls
set /p !number%inputCount%!=Input %inputCount%:
set /a inputCount=%inputCount%+1
if %inputCount% geq 3 goto output
goto input
:output
rem cls
echo !number%outputCount%!
set /a outputCount=%outputCount%+1
if %outputCount% geq 3 goto exit
goto output
:exit
rem pause
echo exit
set number
Output is at end just a line with NUMBER_OF_PROCESSORS=2. There are no environment variables number0, number1, number2 which would be also output by set number. And the command line echo !number%outputCount%! in file results three times in the information that ECHO is OFF.
The reason can be seen on looking on the output command lines really executed after preprocessing each line by Windows command interpreter.
set /p !number%inputCount%!=Input %inputCount%:
The string entered by the user, if not just RETURN or ENTER was hit by the user on prompt, should be assigned to the environment variables of which name are stored in the environment variables number0, number1 and number2. But the environment variables number0, number1 and number2 are never defined by your batch file as your intention is to store the input strings into the variables with name number0, number1 and number2. So this command line is finally on execution:
set /p =Input 0:
set /p =Input 1:
set /p =Input 2:
Those command lines would result in an exit of batch processing because of a syntax error, but this does not occur here because of usage of delayed expansion as it can be seen in the console window.
The solution is a batch code as follows:
#echo off
setlocal EnableExtensions EnableDelayedExpansion
set "Index=0"
:Input
cls
set /A Index+=1
set /P "Number%Index%=Input %Index%: "
if not %Index% == 3 goto Input
cls
set "Index=0"
:Output
set /A Index+=1
if defined Number%Index% echo Number%Index%=!Number%Index%!
if not %Index% == 3 goto Output
endlocal
The output of this batch file on entering on first prompt Hello!, on second prompt nothing and on third prompt Bye! is:
Number1=Hello!
Number3=Bye!
Okay, we have not entered a number as we have the freedom to type anything from nothing to bad strings like Double quote " or | or < or > are bad inputs on user prompt on being prompted for an input. But the batch file works as expected now.
Further please note that the string after set /A is an arithmetic expression which is parsed completely different to any other string on a command line. For examples the current values of environment variables can be referenced by using just the variable name without surrounding percent signs or exclamation marks. That make it possible to use variables in arithmetic expression within an IF or FOR command block without usage of delayed expansion. The help output on running set /? in a command prompt window explains this different parsing behavior quite good as well as which operators can be used like += in the arithmetic expression.
For understanding the used commands and how they work, open a command prompt window, execute there the following commands, and read entirely all help pages displayed for each command very carefully.
cls /?
echo /?
endlocal /?
goto /?
if /?
rem /?
set /?
setlocal /?
Some more hints:
Don't use just exit in a batch file, use exit /B or goto :EOF, see Where does GOTO :EOF return to?
After verification that the user entered anything at all and the entered string is really a number (decimal, octal or hexadecimal), make sure to process the number right according to your and the users' expectations. What I mean here is demonstrated with:
#echo off
set "Number=020"
set /A Number+=1
echo Result=%Number%
pause
What do you expect as result, 21 or the real output result 17?
A number string starting with 0 is interpreted as octal number. A number string starting with 0x or 0X is interpreted as hexadecimal number. Change 020 to 008 and the result is 1. Why? 008 is invalid for an octal number and therefore replaced by 0 which is incremented by one.

How can I run a command with a variable? /// How can I produce a random number with with a variable?

I'm aware that set zeroThroughNine=%Random%*9/32768 followed by echo %zeroThroughNine% will produce a a random number between and including 0 and 9. But it seems the interpreter doesn't evaluate the contents of the variable every time it is called, and as such, echo %zeroThroughNine% produces, for example, 7 every time.
I looked up a method for running commands using variables so that I could try to force it to work. I liked the question because it was very basal in its approach; something along the lines of "How can I run commands using variables?", tagged appropriately. I didn't much care for the answer because it was very narrow. The highest voted and selected answer was:
Simple. Just run set commandVar=echo "Hello world.", followed by echo %commandVar%.
Of course the truth is that only works for the echo command. >: [
Anyway I'll stop complaining. This is what I've tried:
set zeroThroughNine=set /a number=%Random%*9/32768 & echo %number% followed by echo %zeroThroughNine%
Unfortunately the & echo %number% section of my SET command runs immediately, producing "%number%" as output --and using echo %zeroThroughNine% produces "set /a number=8436*9/32768", for example, as output.
So two questions: How can I universally achieve running commands with the use of variables (or some alternative method), and perhaps more pressing, how can I achieve producing a new random number at the command line with each new command calling?
You should set number before you set zeroThroughNine to the command, like so:
set /a number=%Random%*9/32768
set zeroThroughNine=echo %number%
%zeroThroughNine%
Also, since zeroThroughNine already is an echo command, you don't need to add the extra echo before it.
EDIT:
Taking into account your Random calculation is needlessly complicated, the final code should be something like this (1 - 10 exclusive):
set /a number=%Random% %% 10
set zeroThroughNine=echo %number%
%zeroThroughNine%
Important thing is, rather than trying to do it all on one line, it is much more readable by separating it into two.
The CALL SET syntax allows a variable substring to be evaluated, the CALL page has more detail on this technique, in most cases a better approach is to use Setlocal EnableDelayedExpansion.
Command line (note that all % percent signs are escaped as ^% and that > and & characters are escaped within a pair of " double quotes:
set "zeroThroughNine=call set /a number=^%Random^% ^% 10>nul & call echo number=^%number^%"
%zeroThroughNine%
for /L %G in (1, 1, 10) do #%zeroThroughNine%
Batch script, CALL method (note that all % percent signs are escaped as %%):
#echo OFF
SETLOCAL
set "_zeroThroughNine=call set /a _number=%%Random%% %%%% 10 & call echo number=%%_number%%"
echo check variables
set _
echo output
%_zeroThroughNine%
for /L %%G in (1,1,10) do %_zeroThroughNine%
echo check variables after evaluating
set _
ENDLOCAL
Batch script, EnableDelayedExpansion only for output:
#echo OFF
SETLOCAL EnableExtensions DisableDelayedExpansion
set "_zeroThroughNine=set /a _number=!Random! %% 10 & echo Number=!_number!"
SETLOCAL EnableDelayedExpansion
echo check variables
set _
echo output
%_zeroThroughNine%
for /L %%G in (1,1,10) do %_zeroThroughNine%
echo check variables after evaluating
set _
ENDLOCAL
ENDLOCAL
Batch script, EnableDelayedExpansion script wide (note that ! exclamation sign is escaped as ^!):
#echo OFF
SETLOCAL EnableExtensions EnableDelayedExpansion
set "_zeroThroughNine=set /a _number=^!Random^! %% 10 & echo NUMBER=^!_number^!"
echo check variables
set _
echo output
%_zeroThroughNine%
for /L %%G in (1,1,10) do %_zeroThroughNine%
echo check variables after evaluating
set _
ENDLOCAL
Check out this question. Basically, it's an issue with how the %random% environment variable works...
EDIT:
To elaborate, the reason your random value was always 7 is because of how cmd's pseudo-random number generator works, not because of how the variables are interpreted. The matter is explained very well in this answer.
Essentially, in repeated runs of a batch file, %RANDOM% will produce a value very close to the previous run. Thus, the expression %RANDOM%*9/32768 produces the same result in each separate run because of the random value.
If I understand correctly, the question you're asking is how to better generate a random value 0 - 9 inclusive, which would be by using the following expression:
set /a zeroThroughNine=%RANDOM% %% 10

Verify 'X' number of digits have been inputted in batch script

Writing a batch script. How can I verify '3' NUMERIC digits have been inputted by a user with the "/p" prompt?
Prompt to user:
SET /P SITEID=ENTER SITE # (i.e. 001 - MUST BE 3 DIGITS):
I need to verify 3 digits have been inputted, if good continue with script. If NOT good re-prompt user to message of my choosing.
#echo off
setlocal
set "Input="
:Prompt
set /p "Input=ENTER SITE # (i.e. 001 - MUST BE 3 DIGITS): "
if not defined Input goto Prompt
set "Input=%Input:"=%"
for /f "delims=0123456789" %%A in ("%Input%") do goto Prompt
for /f "tokens=1* delims=0" %%A in ("10%Input%") do set "Input=%%B"
if %Input%0 geq 10000 goto Prompt
set "Input=000%Input%"
set "Input=%Input:~-3%"
echo Success = %Input%
pause
endlocal
exit /b 0
Script Explanation:
Prompt for Input
Validate Input
Remove Poison Quotation Characters
Verify Input is only Numbers
Remove leading 0's for comparison
Verify Input is less than 1000
Add back leading 0's
Display Success
If any validation fails, the user is prompted again
Update:
Fix leading 0 removal
Add example of how to add back leading 0's
:loop
SET /P "SITEID=ENTER SITE # (i.e. 001 - MUST BE 3 DIGITS):"
echo("%SITEID:"= %"|findstr /rbe /c:"""[0-9][0-9][0-9]""" >nul || ( echo FAIL & goto loop )
It takes the %SITEID% variable, removes quotes if present and send the data into findstr to test against a regular expression (/r switch) : at the begining of data (/b switch) initial quote (from the echo command) three numeric characters, a closing quote (from the echo command) and end of string (/e switch). If the findstr does not found a match, errorlevel is set and the code after || is executed, printing a message to console an returning to the :loop label to ask again.

Abnormal behavior of batch script in If else condition

As a beginner in batch file programming, i have created a batch file. Below is the code snippet-
SET INDEX=1
SET CURRJOBS=10
REM TOTALJOBS and CURRJOBS are dynamic but to keep code here, i have put static values to them
SET TOTALJOBS=1000
IF [%CURRJOBS%] LSS [%TOTALJOBS%] (
IF [%INDEX%] GEQ [5] (
SET /A INDEX=0
)
ECHO Started at %date% %time% with %CURRJOBS% jobs>>%CURRDIR%\JobSubmit.log
REM Here is a call to another bat file with Index.
ECHO Finished at %date% %time% with %CURRJOBS% jobs>>%CURRDIR%\JobSubmit.log
SET /A INDEX+=1
GOTO START
)ELSE (
ECHO Finished at %date% %time% with %CURRJOBS% jobs>>%CURRDIR%\JobSubmit.log
)
Now, this code, sometimes work, sometimes not.
however there is some syntax error which might be a cause to behave abnormally. Is there any IDE or online utility to check the syntax of batch file?
What is wrong with above code?
Comparisons in IF command are of two types: string or number. To indicate IF that we want number comparison, the numbers must be written with no additional characters. So, your code should be written this way:
IF %CURRJOBS% LSS %TOTALJOBS% (
IF %INDEX% GEQ 5 (
SET /A INDEX=0
)
When a variable or parameter may have an empty value, it is customary to enclose it between quotes to avoid syntax errors, for example:
IF "%POSSIBLEEMPTYVAR%" NEQ "" (
If the variable have string values, you may use the same format for both check for empty value and do the comparison:
IF "%VARIABLE%" equ "THIS VALUE" GOTO OK
However, if a variable may be empty and you want to compare it as number, both tests must be made.

I need to match or replace an asterisk * in a batch environmental variable using only native Windows commands. Is this possible?

I'm trying to remove an asterisk from an environmental variable string, but can't seem to do it.
I'm creating an m3u file based around search strings, so for instance I if I want to make an m3u file containing every song with the word love in it, I would enter:
m3u *Love*
And m3u.bat would create the file:
xLovex.m3u
But the regular method of replacing characters does not work with an asterisk. (Though I don't have that problem with the question mark.)
set nam=%nam:*=x%.m3u
Instead creates the filename
x.m3u
The easy answer is no.
The problem that you're encountering stems from the fact that the asterisk * is a special character when used with the SET search and replace method. It matches multiple characters in a limited, but still useful, way. You can learn about that here.
The hard answer is Yes!
I will provide you with two solutions. One an incomplete solution but elegent,
the other complete and inelegent.
Both methods will search for * and replace it with an x.
Both methods will both search and modify the following string:
*love*
The first method that comes to mind is using a 'FOR /L' statement, and requires that you know how many characters long the environmental variable is.
::Major Edit::
I thought I knew the various maximum size strings of environmental variables, but dbenham has taken me to school, shown me a kick-in-the-behind length function, and in the mean time completely reversed my opinions of the two solutions I'm presenting.
Other than for the Windows 95/98/ME limitation of a 256 Character maximum environmental variable size. It seems that all versions of Windows using CMD.EXE have a limitation of 8,192 characters, well below what the documentation suggests.
Both versions require delayed environmental variable expansion, but for two different reasons. One because I'm operating inside a FOR statement. The other because you cannot put a % pair inside another % pair because the command processor matches the second % that it encounters to the first one it encounters, but we need to use a variable inside another variable expression. (You'll see.)
This solution uses the strLen function (in line 3) from DosTips.com that can be found Here. Just slap it into a file called strLen.bat and be amazed at it's speed!
Solution 1: (FOR /L Solution) :: Preferred Solution ::
setlocal ENABLEDELAYEDEXPANSION
set nam=*love*
rem calling strLen
call :strLen nam len
for /l %%x in (0,1,%len%) do if not "!nam:~%%x,1!"=="" if "!nam:~%%x,1!"=="*" (
set /a plusone=%%x+1
for /l %%y in (!plusone!, 1, !plusone!) do (
set nam=!nam:~0,%%x!x!nam:~%%y!
)
)
echo %nam%
ENDLOCAL
I think this is a quick and elegant solution It could be sped up by adding the contents of strLen.bat to the routine, but I wanted no confusion as to the author.
If you, for some reason, do not wish to use strLen, then the next quickest method would probably use a GOTO loop.
Solution 2: (Goto Solution)
setlocal ENABLEDELAYEDEXPANSION
set nam=*love*
set num=0
:loop
set /a plusone=%num%+1
if "!nam:~%num%,1!"=="*" set nam=!nam:~0,%num%!x!nam:~%plusone%!
set /a num=%num%+1
if not "!nam:~%num%,1!"=="" goto :loop
echo %nam%
EndLocal
Special thanks to dbenham for pointing out the strLen function. It works faster than any batch based function has a right to!
Although there were already some very good and robust ways explained here, I'd still like to add another option for the sake of completion.
It's not as good as the other options but I personally use it in some cases where I'd like to keep the code clean and where I know that it will suffice:
The way it works is by using for /f's delims to cut the string into two parts, which are then put back together, getting rid of the * in the process:
for /f "tokens=1,* delims=*" %%a in ("a*b") do (set string=%%a%%b)
>>> string=ab
Obviously, the downside to this is that it can only be used to remove one *.
To remove more, we can either just use more tokens...
for /f "tokens=1-3,* delims=*" %%a in ("a*b*c*d") do (set string=%%a%%b%%c%%d)
>>> string=abcd
... or we can put the first line in a for /l-loop:
setlocal enableDelayedExpansion
set string=a*b*c*d
for /l %%a in (1, 1, 3) do (
for /f "tokens=1,* delims=*" %%b in ("!string!") do (set string=%%b%%c)
)
>>> string=abcd
Another thing to note is that you can define more than one character in delims, and they will all be removed at once:
for /f "tokens=1,* delims=+-*/" %%a in ("a*-/+b") do (set string=%%a%%b)
>>> string=ab
Another solution to the stated problem is to use a PowerShell replace command within your batch script.
set var=*Love*
echo %var%>var.txt | powershell -command "((get-content var.txt) -replace '[\x2A]','x') -replace '.{1}$' | set-content var.txt"
set /p var=<var.txt
set var=%var%.m3u
echo %var%
In the above code, the second line
writes your string into a text file
calls a PowerShell command to get the contents of that file
replaces the * character with null
overwrites the text file with the new value
Once that is done, you read the value back into your variable.
To further explain the replace command, the first single quotes is what you are searching for. We are using square brackets to identify the * character as a hex character (\x2A is the hex value for *). After the comma, the second set of single quotes contains no value so that the searched object is removed. To prevent a space between xLovex and the .m3u, we have to use -replace '.{1}$' before writing the result to the text file.
Once you are done with the text file, enter a line to delete it.
if exist var.txt del var.txt
Here is an approach that does not walk through all characters of a string, but it uses a for /F loop to split the string at every occurrence of a (sequence of a) certain character. The actual functionality is packed into a sub-routine for easy reuse, so the main section of the following script just contains some code to test:
#echo off
setlocal EnableExtensions DisableDelayedExpansion
::This is the main routine of the script holding code for test and demonstration:
rem // Definition of some sample text to test (note that `%%` becomes one literal `%`):
set "DATA=some text,"^&"&;0'#%%~#`$:wild**card*?.re<dir>=|+([{parens}])-^/equal==to=!_"
echo/
call :REPL_CHAR TEXT DATA "*" "?"
setlocal EnableDelayedExpansion
echo(In: !DATA!
echo(Out:!TEXT!
echo/
echo(In: !TEXT!
call :REPL_CHAR TEXT TEXT "=" "/"
echo(Out:!TEXT!
endlocal
endlocal
exit /B
:REPL_CHAR
::This function replaces in a string every occurrence of a sequence of a certain character
::by another character or a string. It even correctly handles the characters `*` and `=`.
:: USAGE:
:: call :REPL_CHAR ref_output_string ref_input_string val_search_char val_replace_char
:: PARAMETERS:
:: ref_output_string reference to (name of) variable to receive the resulting string;
:: ref_input_string reference to variable that holds the original string; if empty
:: (`""`), the variable referenced by `ref_output_string` is used;
:: val_search_char single character that is to be replaced;
:: val_replace_char character or string to replace every sequence of `val_search_char`
:: with; this may even be empty;
rem // Localise environment and detect whether delayed expansion is enabled (needed later):
setlocal & set "$NDX=!"
setlocal DisableDelayedExpansion
rem // Fetch arguments and verify them:
set "#RET=%~1" & if not defined #RET endlocal & endlocal & exit /B 2
set "#STR=%~2" & if not defined #STR set "#STR=%#RET%"
set "CHR=%~3"
if not defined CHR endlocal & endlocal & exit /B 1
set "RPL=%~4"
setlocal EnableDelayedExpansion
rem // Initialise several auxiliary variables:
set "TST=!%#STR%!" & set "CHR=!CHR:~,1!" & set "INS="
if "!CHR!"=="_" (set "BUF=#" & set "WRK=!TST!#") else (set "BUF=_" & set "WRK=!TST!_")
:REPL_CHAR_LOOP
rem // Check whether the end of the string has been reached:
if not defined TST set "BUF=!BUF:~1,-1!" & goto :REPL_CHAR_NEXT
rem // Split the string at the next sequence of search characters:
for /F tokens^=1*^ delims^=^%CHR%^ eol^=^%CHR% %%S in ("!BUF!!INS!!WRK!") do (
rem // Store the portions before and after the character sequence:
endlocal & set "BUF=%%S" & set "TST=%%T" & set "WRK=%%T" & setlocal EnableDelayedExpansion
)
rem // Loop back and find the next character sequence:
set "INS=!RPL!" & goto :REPL_CHAR_LOOP
:REPL_CHAR_NEXT
rem // Return the resulting string with all special characters properly handled:
if not defined $NDX if defined BUF set "BUF=!BUF:"=""!^"
if not defined $NDX if defined BUF set "BUF=!BUF:^=^^^^!"
if not defined $NDX if defined BUF set "BUF=%BUF:!=^^^!%" !
if not defined $NDX if defined BUF set "BUF=!BUF:""="!^"
for /F "delims=" %%S in (^""!BUF!"^") do endlocal & endlocal & endlocal & set "%#RET%=%%~S" !
exit /B
The input and output data of this script (let us call it repl_char_demo.bat) are:
>>> repl_char_demo.bat
In: some text,"&"&;0'#%~#`$:wild**card*?.re<dir>=|+([{parens}])-^/equal==to=!_
Out:some text,"&"&;0'#%~#`$:wild?card??.re<dir>=|+([{parens}])-^/equal==to=!_
In: some text,"&"&;0'#%~#`$:wild?card??.re<dir>=|+([{parens}])-^/equal==to=!_
Out:some text,"&"&;0'#%~#`$:wild?card??.re<dir>/|+([{parens}])-^/equal/to/!_
This is a script that uses for /L loops to walk through all characters of the string, to check each character against a predefined one and replaces it as specified. This method replaces every single matching character rather than sequences. Again the functionality is put into a sub-routine (the main section is dismissed this time):
:REPL_CHAR
::This function replaces in a string every occurrence of one certain character by another
::character or a string. It even correctly handles the characters `*` and `=`, as well as
::sequences of search characters so that every single one becomes replaced.
:: USAGE:
:: call :REPL_CHAR ref_output_string ref_input_string val_search_char val_replace_char
:: PARAMETERS:
:: ref_output_string reference to (name of) variable to receive the resulting string;
:: ref_input_string reference to variable that holds the original string; if empty
:: (`""`), the variable referenced by `ref_output_string` is used;
:: val_search_char single character that is to be replaced;
:: val_replace_char character or string to replace every single `val_search_char`
:: with; this may even be empty;
rem // Localise environment and detect whether delayed expansion is enabled (needed later):
setlocal & set "$NDX=!"
setlocal DisableDelayedExpansion
rem // Fetch arguments and verify them:
set "#RET=%~1" & if not defined #RET endlocal & endlocal & exit /B 2
set "#STR=%~2" & if not defined #STR set "#STR=%#RET%"
set "CHR=%~3"
if not defined CHR endlocal & endlocal & exit /B 1
set "RPL=%~4"
setlocal EnableDelayedExpansion
rem // Initialise several auxiliary variables:
set "WRK=!%#STR%!" & set "CHR=!CHR:~,1!" & set "BUF="
rem // Loop through all characters and check for match:
if defined WRK for /L %%J in (0,1,63) do for /L %%I in (0,1,127) do (
set /A "POS=%%J*64+%%I" & for %%P in (!POS!) do (
set "TST=!WRK:~%%P,1!" & if not defined TST goto :REPL_CHAR_QUIT
rem // Store character or replacement depending on whether there is a match:
if "!TST!"=="!CHR!" (set "BUF=!BUF!!RPL!") else (set "BUF=!BUF!!TST!")
)
)
:REPL_CHAR_QUIT
rem // Return the resulting string with all special characters properly handled:
if not defined $NDX if defined BUF set "BUF=!BUF:"=""!^"
if not defined $NDX if defined BUF set "BUF=!BUF:^=^^^^!"
if not defined $NDX if defined BUF set "BUF=%BUF:!=^^^!%" !
if not defined $NDX if defined BUF set "BUF=!BUF:""="!^"
for /F "delims=" %%S in (^""!BUF!"^") do endlocal & endlocal & endlocal & set "%#RET%=%%~S" !
exit /B
There are actually two nested for /L loops rather than a single one, both of which become broken as soon as the end of the string is reached, using the goto command. Breaking a for /L loop means that it completes iterating in the background although its body is no longer executed. Therefore, using a single loop takes much more time to finish after being broken rather than two nested ones.
The input and output data of this script (with the same main section as above) are:
>>> repl_char_demo.bat
In: some text,"&"&;0'#%~#`$:wild**card*?.re<dir>=|+([{parens}])-^/equal==to=!_
Out:some text,"&"&;0'#%~#`$:wild??card??.re<dir>=|+([{parens}])-^/equal==to=!_
In: some text,"&"&;0'#%~#`$:wild??card??.re<dir>=|+([{parens}])-^/equal==to=!_
Out:some text,"&"&;0'#%~#`$:wild??card??.re<dir>/|+([{parens}])-^/equal//to/!_
See this answer, and with set-ast.bat you'll want to put set-ast nam "x" in your file where needed.
set-ast takes the parameters <variable-to-modify> <string-to-replace-asterisks-with>

Resources