This is a fairly general question on user input validation, with particular focus on special characters. In this "simplest" case one character validation is sought.
The code is modified from a 5 year old thread but it is apparent that most of the special characters require extra treatment to what is proposed below:
Setlocal EnableDelayedExpansion
:Prompter
REM Eliminate poison chars on <CR>
SET "validateText="
SET /P "validateText=Type f to foo or b to bah. (f/b)"
call :Validate
if /i %validateText%==f (echo foo pause>nul goto :eof
) else (
if /i %validateText%==b (echo bah pause>nul goto :eof
) else (goto Prompter))
:Validate
SET Input=%validateText:~0,1%
For %%G In (^= ^| ^& ^< ^> ^^ + ^( ^) \ / . # # $ { } [ ] ' : ` ^%% ^") Do (
REM Test Script fails for = | & < > ^ ) ; " and space
REM Loops through the set, but the if clauses are never evaluated
If %Input%==%%G (#echo isequal pause>nul exit /b) else (echo isnotequal pause>nul exit /b))
REM quotes on the test clause also fail
rem If "%validateText%==%%G" (echo equal pause >nul goto :eof) else (echo not equal pause >nul goto :eof))
REM The following are what is intended. All fail
rem If "%validateText%==%%G" (set Input=!Input:%%G=! goto BangDO) else (set Input=%validateText% goto :eof))
rem If %validateText%==%%G (set "Input=%Input:%%G=%" goto BangDO) else (set "Input=%validateText%" goto :eof))
:BangDO
#echo %Input%
pause
Set Input=!Input:%=!
REM Remove !
SetLocal DisableDelayedExpansion
Set "%Input:!=X"
Setlocal EnableDelayedExpansion
Set "validateText=%Input%"
#echo %validateText%
pause
The question is, that given there haven't been updates to the Command processor (at least since the quoted post) why it doesn't work. Either improve on this attempt at validation or provide own code.
The answer is almost staring you in the face :-) You enable delayed expansion, but then you fail to use it!
One beautiful feature of delayed expansion is you never have to worry about poison characters in the content.
One other thing, set /p will preserve any existing variable value if the user simply presses <Enter> without entering a value. That may or may not be what you want. I usually explicitly clear the variable value before using set /p
setlocal EnableDelayedExpansion
REM set /p does not clear existing value if user enters empty string
REM Remove REM below if you want to ignore any existing str value
REM set "str="
:prompter
set /p "str=Type f to foo or b to bah. (f/b) "
if /i !str!==f (
echo foo
) else if /i !str!==b (
echo bah
) else goto prompter
pause >nul
In your case only one replacement is necessary to be secure.
SET "validateText="
SET /P "validateText=Type f to foo or b to bah. (f/b)"
set "validateText=%validateText:"=""%"
if /i "%validateText%"=="f" (
echo foo pause>nul goto :eof
) else if /i "%validateText%"=="b" (
echo bah pause>nul goto :eof
) else (
goto Prompter
)
This works as all special characters are safe inside quotes.
And you only need to be sure that a quote inside your text can't cause problems, this can be achieved by doubling all quotes.
Related
I have two labels in my batch file. The initial label MAIN shall stay in control, so it Calls the second label, which ends with exit /b.
My script's Main label Calls the other, passing it arguments, which will be used to search strings wothin a text file.
When returning to the Calling label, it slways receives an empty return string.
I think this has something to do with the variable expansion in a loop. Who knows?
Here is the Script:
#echo off
SETLOCAL ENABLEDELAYEDEXPANSION
:MAIN
call :getReturnValue "1234 0815 4321 12815" "readBackVal"
if !errorlevel! equ 0 (
echo readback=!readBackVal!
echo readback=%readBackVal%
)
pause
exit /b 0
REM Function, which checks if the give return value is in a specific textfile (line for line check)
:getReturnValue
set "myExpectedValueList=%~1"
set "retval=%~2"
set "file=textexample.txt"
for %%i in (%myExpectedValueList%) do (
for /f "tokens=*" %%a in (%file%) do (
echo %%a|findstr /r "^.*%%i$"
)
if !errorlevel! equ 0 (
(endlocal
set /a "%retval%=%%i")
)
exit /b 0
)
)
exit /b 1
Here is the sample textfile textexample.txt:
Setup returns with errorcode=0815
Here is the answer i looked for:
Hi, first i want to inform that i made some changes due to the Answer of
#OJBakker. This changes are listed at the bottom of the script.
The problem was to return a value from a called function/label to the calling function/label. The stich here is, that the magic
is done in the (endlocal...) section of the called function/label -> means the return of the variable.
Before the endlocal command is executed, the compiler replaces the variables in this section by their values and afterwards executes the command´s from left to right. Means following:
First, the compiler sees following:
(endlocal
if "%retval%" neq "" (call set /a %retval%=%%i)
)
Second, the compiler replaces the variables by their values:
(endlocal
if "readBackVal" neq "" (set /a "readBackVal"=1815)
)
Third: This command is executed
(endlocal
if "readBackVal" neq "" (set /a "readBackVal"=1815)
)
Now here is my complete script (i also fixed some other problems with it which i commented at the bottom of the script
#echo off
SETLOCAL ENABLEDELAYEDEXPANSION
:MAIN
setlocal
call :getReturnValue "1234 1815 4321 12815" "readBackVal"
if "!errorlevel!" equ "0" (
echo readback=!readBackVal!
)
pause
exit /b 0
REM Function, which checks if the give return value is in a specific textfile (line for line check)
:getReturnValue
setlocal
set "myExpectedValueList=%~1"
set "retval=%~2"
set "file=textexample.txt"
for %%i in (%myExpectedValueList%) do (
for /f "tokens=*" %%a in (%file%) do (
echo %%a|findstr /r "^.*%%i$" >NUL
)
if "!errorlevel!" equ "0" (
(endlocal
if "%retval%" neq "" (set /a %retval%=%%i)
)
exit /b 0
)
)
exit /b 1
REM Changes to initial posting:
REM Added "setlocal" keyword to the function "getReturnValue"
REM Corrected an invalid paranthesis in the (endlocal...) section
REM Changed the file "textexample.txt" -> 0815 to 1815 to remove leading zero (findstr. Problem),
REM Added check, if parameter "retval" has been passed to the called function e.g. is not empty
REM FINAL -> applied double variable expansion (call set /a ...) to return the value proper
REM to the :MAIN function.
#echo off
SETLOCAL ENABLEDELAYEDEXPANSION
:MAIN
call :getReturnValue "1234 0815 4321 12815" "readBackVal"
if %errorlevel% equ 0 (echo readback=%readBackVal%)
pause
endlocal
exit /b 0
REM Function, which checks if the give return value is in a specific textfile (line for line check)
:getReturnValue
set "myExpectedValueList=%~1"
set "retval=%~2"
set "file=textexample.txt"
for %%i in (%myExpectedValueList%) do (
for /f "tokens=*" %%a in (%file%) do (
echo %%a| >con 2>&1 findstr /r "^.*%%i$"
if !errorlevel! equ 0 (
set /a "%retval%=%%i"
exit /b 0
)
)
)
exit /b 1
rem changes:
rem endlocal moved to main.
rem check for errorlevel moved to within the commandblock of the inner for-loop.
rem 'exit /b 0' moved to within the if. This exit line stopped the for after the first item.
rem redirection added to findstr command. Now the output shows the remaining problem.
rem Invalid number. Numeric constants are either decimal (17), hexadecimal (0x11), or octal (021).
rem Findstr really does not like the value 0815, especially the starting zero.
rem I am not sure how to change the regexp so findstr won't barf at the leading zero.
rem Maybe someone else can solve this remaining problem.
Just noticed that VAR=%VAR:*STRING% does eliminate the previous string but VAR=%VAR:STRING*% doesn't elimiate next string
so how to eliminate the next string ? my current code is :
:CheckEnvironmentVariable Location Variable Value
IF [%1] EQU [System] (
ECHO Querying system
) ELSE (
IF [%1] EQU [User] (
ECHO Querying User Environments
FOR /F "usebackq tokens=*" %%x IN (`REG QUERY "HKCU\Environment"`) DO (
SET CURRVARS=%%x&&SET CURRVARS=!CURRVARS:REG_*=!
ECHO !CURRVARS!
)
) ELSE (
ECHO ERROR ^^! Invalid Environment Variable Location "%1"
)
)
EXIT /B
which is doen't work as expected
I am afraid I don't understand what you want to do. However, I guess that is related to this "possible" solution:
#echo off
setlocal EnableDelayedExpansion
set "VAR=This is a STRING long value"
echo VAR: %VAR%
rem Eliminate string "previous" to "STRING" (including it)
set "tail=%VAR:*STRING=%"
echo Tail: "%tail%"
rem Eliminate string "next" to "STRING" (not including it)
set "head=!VAR:%tail%=!"
echo Head1: "%head%"
rem Eliminate string "next" to "STRING" (including it)
set "head=!VAR:STRING%tail%=!"
echo Head2: "%head%"
Output:
VAR: This is a STRING long value
Tail: " long value"
Head1: "This is a STRING"
Head2: "This is a "
Based upon the code you have submitted, I'm not even sure why you would want to try to split the string at that particular place. There is a consistent string in every single line that would be returned by your reg.exe command, and that is REG_. The beauty of that paricular string is that it will always be non space separated, non localized, and never contain special characters. If you split at that point, you know that the substring you're looking for, will always be every token following its remainder, e.g. EXPAND_SZ your string(s); SZ your string(s).
So here's some example code which uses that method, but please be aware that it wil not work as intended should you have any variable defined within the System or User Environments with names including the case insensitive string REG_:
#Echo Off
SetLocal EnableExtensions DisableDelayedExpansion
:Ask4Var
ClS
Set "sName="
Set /P "sName=Please enter the name of the variable you wish to verify>"
If Not Defined sName GoTo Ask4Var
(
Set %sName%
) 2>NUL | %SystemRoot%\System32\findstr.exe /BIL "%sName%=" 1>NUL || (
Echo There is no variable named %sName% in the current environment
%SystemRoot%\System32\timeout.exe /T 3 1>NUL
GoTo Ask4Var
)
Set "Env=System"
Set "RootKey=HKLM"
Set "SubKey=System\CurrentControlSet\Control\Session Manager\Environment"
%SystemRoot%\System32\choice.exe /C SU /N /M "[S]ystem OR [U]ser?"
If ErrorLevel 2 (
Set "Env=User"
Set "RootKey=HKCU"
Set "SubKey=Environment"
)
Set "Reg=%SystemRoot%\System32\reg.exe"
Set "ValueString="
For /F Delims^=^ EOL^= %%G In (
'%Reg% Query "%RootKey%\%SubKey%" /V /F "%sName%" /E ^|
%SystemRoot%\System32\find.exe "REG_"'
) Do (
Set "Result=%%G"
SetLocal EnableDelayedExpansion
For /F "Tokens=1,*" %%H In ("!Result:*REG_=!") Do (
EndLocal
Set "ValueString=%%I"
)
)
If Not Defined ValueString (
Echo There is no variable named %sName% in the %Env% environment
%SystemRoot%\System32\timeout.exe /T 3 1>NUL
GoTo Ask4Var
)
Echo The expanded string value of %sName% is %ValueString%.
%SystemRoot%\System32\timeout.exe /T 7 1>NUL
GoTo :EOF
Take this simple example:
#ECHO OFF
SET /P phrase="Enter Word : "
SET /a rnum=%random% %%10 +1
ECHO %phrase%
ECHO %rnum%
SET rchar=%phrase:~0,%rnum%%
ECHO %rchar%
Pause
I just want to be able to pass that rnum variable to pick that as the character chosen from the left of that user entered word to that random character.
I can't seem to figure out how to pass that as a variable.
I tried with enabledelayedexpansion with no luck:
#ECHO OFF
SET /P Phrase="Enter Word : "
SET /a rnum=%random% %%10 +1
ECHO %phrase%
ECHO %rnum%
setlocal enabledelayedexpansion
SET rchar=!phrase:~0,%rnum%!
endlocal
ECHO %rchar%
Pause
So how do I pass rnum as a variable in this instance? Thanks for any assistance.
Here's a simple modification of your example delayed expansion code, which shows one method of maintaining your variable value beyond endlocal:
#ECHO OFF
SET /P "phrase=Enter Word : "
SET /A rnum = %RANDOM% %% 10 + 1
ECHO %phrase%
ECHO %rnum%
SETLOCAL ENABLEDELAYEDEXPANSION
FOR %%G IN ("!phrase:~0,%rnum%!") DO ENDLOCAL & SET "rchar=%%~G"
ECHO rchar=%rchar%
PAUSE
The above example should be fine, as long as the end user does not begin to input strings with problematic characters. If you wanted to make it a little more robust for such scenarios then perhaps this will help:
#Echo Off
SetLocal EnableExtensions DisableDelayedExpansion
:AskString
Rem Get interactive string input
Set "String="
Set /P String="Enter Word : "
If Not Defined String GoTo AskString
Set String
Rem Generate a random integer 1..10
Set /A "Integer = (%RANDOM% %% 10) + 1"
Set Integer
Rem Create a substring variable using %String% and %Integer%
Echo %%SubString%% = %%String:~0,%Integer%%%
SetLocal EnableDelayedExpansion
For /F Delims^=^ EOL^=^ UseBackQ %%G In ('"!String:~0,%Integer%!"') Do (
EndLocal
Set "SubString=%%~G"
)
Set SubString
Pause
Please note that the above code uses Set Variable to display the variable name along side its value. If your variable contains certain poison characters just using Echo %Variable% may not work, and you would probably be better off keeping delayed expansion enabled at that time.
As Compo already comments, the position of your endlocal is the problem.
You could just move the endlocal after the echo
#ECHO OFF
setlocal enabledelayedexpansion
SET /P Phrase="Enter Word : "
SET /a rnum=%random% %%10 +1
ECHO !phrase!
ECHO !rnum!
SET "rchar=!phrase:~0,%rnum%!"
ECHO !rchar!
endlocal
Is it any way to limit the length of a batch variable? I mean, if it is possible to program a variable that only admits between 0 and x characters? So, for an instance, if I entered 123456 and the max length was 4 it wouldn't proceed to continue. I hope you can understand my question.
Thanks in advance.
Demonstration batch code according to suggestions of aschipfl and rojo:
#echo off
setlocal EnableExtensions EnableDelayedExpansion
:UserPrompt
cls
set "UserInput="
set /P "UserInput=Enter string with a length between 1 and 4: "
if not defined UserInput goto UserPrompt
if not "!UserInput:~4!" == "" goto UserPrompt
echo/
echo String entered: !UserInput!
echo/
endlocal
pause
!UserInput:~4! is replaced by command processor on execution of the batch file by the string from user input starting with fifth character. First character of a string value has index value 0 which is reason for number 4 for fifth character. This string is empty if user entered a string not longer than 4 characters, otherwise this substring is not empty resulting in user must input again a string.
Delayed expansion is used to avoid an exit of batch processing caused by a syntax error if the user enters a string containing an odd number of double quotes.
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 /?
if /?
pause /?
set /?
setlocal /?
If you mean "limit the length of a batch variable when it is read via SET /P command", then you may use the ReadLine subroutine described at this post, that emulates SET /P command using pure Batch file commands, and just insert the maximum length restriction.
#echo off
setlocal
call :ReadNChars string4="Enter 4 characters maximum: " 4
echo String read: "%string4%"
goto :EOF
:ReadNChars var="prompt" maxLen
rem Read a line emulating SET /P command
rem Antonio Perez Ayala
rem Initialize variables
setlocal EnableDelayedExpansion
echo > _
for /F %%a in ('copy /Z _ NUL') do set "CR=%%a"
for /F %%a in ('echo prompt $H ^| cmd') do set "BS=%%a"
rem Show the prompt and start reading
set /P "=%~2" < NUL
set "input="
set i=0
:nextKey
set "key="
for /F "delims=" %%a in ('xcopy /W _ _ 2^>NUL') do if not defined key set "key=%%a"
rem If key is CR: terminate input
if "!key:~-1!" equ "!CR!" goto endRead
rem If key is BS: delete last char, if any
set "key=!key:~-1!"
if "!key!" equ "!BS!" (
if %i% gtr 0 (
set /P "=!BS! !BS!" < NUL
set "input=%input:~0,-1%"
set /A i-=1
)
goto nextKey
)
rem Insert here any filter on the key
if %i% equ %3 goto nextKey
rem Else: show and accept the key
set /P "=.!BS!%key%" < NUL
set "input=%input%%key%"
set /A i+=1
goto nextKey
:endRead
echo/
del _
endlocal & set "%~1=%input%"
exit /B
However, if you want to limit the length of a Batch variable in other cases, like SET /A or plain SET commands, then there is no way to do that. Of course, you may execute such commands and then cut the variable value to the maximum length, but that process is an entirely different thing.
I need to implement a function in a Windows batch script to get the LastIndexOf a character into a given string.
For example: Given the following string, I need to get the last index of character '/':
/name1/name2/name3
^
So I need to get the value:
12
Joey's solution works, but the character to find is hard coded, and it is relatively slow.
Here is a parametized function that is fast and can find any character (except nul) within the string. I pass the name of variables containing the string and the character instead of string literals so that the function easily supports all characters.
#echo off
setlocal
set "test=/name1/name2/name3"
set "char=/"
::1st test simply prints the result
call :lastIndexOf test char
::2nd test stores the result in a variable
call :lastIndexOf test char rtn
echo rtn=%rtn%
exit /b
:lastIndexOf strVar charVar [rtnVar]
setlocal enableDelayedExpansion
:: Get the string values
set "lastIndexOf.char=!%~2!"
set "str=!%~1!"
set "chr=!lastIndexOf.char:~0,1!"
:: Determine the length of str - adapted from function found at:
:: http://www.dostips.com/DtCodeCmdLib.php#Function.strLen
set "str2=.!str!"
set "len=0"
for /L %%A in (12,-1,0) do (
set /a "len|=1<<%%A"
for %%B in (!len!) do if "!str2:~%%B,1!"=="" set /a "len&=~1<<%%A"
)
:: Find the last occurrance of chr in str
for /l %%N in (%len% -1 0) do if "!str:~%%N,1!" equ "!chr!" (
set rtn=%%N
goto :break
)
set rtn=-1
:break - Return the result if 3rd arg specified, else print the result
( endlocal
if "%~3" neq "" (set %~3=%rtn%) else echo %rtn%
)
exit /b
It wouldn't take much modification to create a more generic :indexOf function that takes an additional argument specifying which occurance to find. A negative number could specify to search in reverse. So 1 could be the 1st, 2 the 2nd, -1 the last, -2 penultimate, etc.
(Note: I'm assuming Windows batch files because, frankly, I have only seen a single question asking for an actual DOS batch file here so far. Most people simply misattribute “DOS” to anything that has a window of gray-on-black monospaced text without knowing what they're actually talking of.)
Just loop through it, updating the index as you go:
#echo off
setlocal enabledelayedexpansion
set S=/name1/name2/name3
set I=0
set L=-1
:l
if "!S:~%I%,1!"=="" goto ld
if "!S:~%I%,1!"=="/" set L=%I%
set /a I+=1
goto l
:ld
echo %L%
I know this question is a bit old now, but I needed a function that could find the location of a substring (of any length) within a string, and adapted dbenham's solution for my purposes. This function also works with individual characters within a string, as asked for in the original question, and can search for specific instances (as suggested by dbenham).
To use this function, the actual strings must be passed. Dbenham does note that this supports fewer characters than passing the actual variables, but I find that this variant is more reuseable (especially with pipes).
The third argument takes the instance that should be found, with negative numbers specifying to search from the end. The index returned is the offset from the start of the string to the first character in the substring.
#ECHO off
SET search_string=sub
CALL :strIndex "The testing subjects subjects to testing." "%search_string%" -2
ECHO %ERRORLEVEL%
PAUSE
EXIT
:strIndex string substring [instance]
REM Using adaptation of strLen function found at http://www.dostips.com/DtCodeCmdLib.php#Function.strLen
SETLOCAL ENABLEDELAYEDEXPANSION
SETLOCAL ENABLEEXTENSIONS
IF "%~2" EQU "" SET Index=-1 & GOTO strIndex_end
IF "%~3" EQU "" (SET Instance=1) ELSE (SET Instance=%~3)
SET Index=-1
SET String=%~1
SET "str=A%~1"
SET "String_Length=0"
FOR /L %%A IN (12,-1,0) DO (
SET /a "String_Length|=1<<%%A"
FOR %%B IN (!String_Length!) DO IF "!str:~%%B,1!"=="" SET /a "String_Length&=~1<<%%A"
)
SET "sub=A%~2"
SET "Substring_Length=0"
FOR /L %%A IN (12,-1,0) DO (
SET /a "Substring_Length|=1<<%%A"
FOR %%B IN (!Substring_Length!) DO IF "!sub:~%%B,1!"=="" SET /a "Substring_Length&=~1<<%%A"
)
IF %Substring_Length% GTR %String_Length% GOTO strIndex_end
SET /A Searches=%String_Length%-%Substring_Length%
IF %Instance% GTR 0 (
FOR /L %%n IN (0,1,%Searches%) DO (
CALL SET StringSegment=%%String:~%%n,!Substring_Length!%%
IF "%~2" EQU "!StringSegment!" SET /A Instance-=1
IF !Instance! EQU 0 SET Index=%%n & GOTO strIndex_end
)) ELSE (
FOR /L %%n IN (%Searches%,-1,0) DO (
CALL SET StringSegment=%%String:~%%n,!Substring_Length!%%
IF "%~2" EQU "!StringSegment!" SET /A Instance+=1
IF !Instance! EQU 0 SET Index=%%n & GOTO strIndex_end
))
:strIndex_end
EXIT /B %Index%