How to ask a question but only able to write numbers? - batch-file

set PenaltyLimit = 0
:NewPass
echo Create a penalty limit
set/p "NewPenaltyLimit=>"
--> if %NewPenaltyLimit%==Numbers?? do (
#echo %NewPenaltyLimit% >> File/PenaltyLimit.txt)
FOR /F "usebackq tokens=* delims=" %%W in ("File/PenaltyLimit.txt") do SET PenaltyLimit=%%W
goto lock
:Fail2
#set /a penalty = %penalty% + 1
if %penalty%==%penaltylimit% goto DELETE
goto FAIL
What would you put in exchange for the line with the "-->"
Instead of breaking the batch file, writing something random, is it possible that you could somehow either stop someone entirely from writing anything but numbers or make it so the batch file will only read numbers? (Someone what example above this text)

#echo off
setlocal
:NewPass
echo Create a password
set/p "newpass=>" || goto :NewPass
:NewPenaltyLimit
cls
echo Create a penalty limit, Numbers only
set /p "NewPenaltyLimit=>" || goto :NewPenaltyLimit
set "NewPenaltyLimit=%NewPenaltyLimit:"=%"
if not defined NewPenaltyLimit goto NewPenaltyLimit
for /f "delims=1234567890" %%A in ("%NewPenaltyLimit%") do goto NewPenaltyLimit
cls
ping localhost -n 2 > nul
echo You chose %NewPenaltyLimit% goto lock
If you delimit the value of %NewPenaltyLimit% by integers
of 1234567890, the do goto :newPass should only happen
if characters other than integers exist.
Due to possibility of double quotes being input to the set /p prompt
which could cause a syntax error when expanded as text when the code
is parsed.
The double quotes will be substituted (removed) from the input value
immediately after input. This means that "86" will become 86 and
will be accepted as a valid number.
Example values:
a1 delimited creates a token with value of a which causes
the goto :NewPenaltyLimit to happen.
12 delimited creates no token as 1 and 2 are both delimiters.
If Delayed Expansion is allowed to be enabled.
This version can handle the input without removing double quotes:
#echo off
setlocal
:NewPass
echo Create a password
set/p "newpass=>" || goto :NewPass
:NewPenaltyLimit
cls
echo Create a penalty limit, Numbers only
set /p "NewPenaltyLimit=>" || goto :NewPenaltyLimit
setlocal enabledelayedexpansion
for /f "delims=1234567890" %%A in ("!NewPenaltyLimit!") do (
endlocal
goto NewPenaltyLimit
)
endlocal
cls
ping localhost -n 2 > nul
echo You chose %NewPenaltyLimit% goto lock
Delayed Expansion is only needed for only the for loop,
so this may be a better version.
!NewPenaltyLimit! is not expanded in the code when the code is parsed,
which means an input such as "86" will not cause error and will not be
accepted as a vaild number, as it has double quotes.
References:
View for /? for more helpful information.
View set /? for the information about substitution used in this answer.
View setlocal /? and if /? about Delayed Expansion.

You can try with embedding a powershell code:
<# : batch portion
#echo off & setlocal
for /f "tokens=*" %%a in ('powershell -noprofile "iex (${%~f0} | out-string)"') do set "number=%%a"
echo you've entered %number%
endlocal
goto :EOF
: end batch / begin powershell #>
do {
[Console]::Error.Write("Enter a number:")
$inputString = read-host
$value = $inputString -as [Double]
$ok = $value -ne $NULL
if ( -not $ok ) {[Console]::Error.WriteLine("You must enter a numeric value") }
}
until ( $ok )
write-host "$value"
One advantage is that this can support few types as Integer,Double,Unsigned integer and so on (in this case it is Double.)
You can try also with embedded jscript code (can be used on old XP machines):
#if (#X) == (#Y) #end /* JScript comment
#echo off
for /f "tokens=*" %%a in ('cscript //E:JScript //nologo "%~f0" "%~nx0" %* ') do set "number=%%a"
echo you've entered %number%
exit /b %errorlevel%
#if (#X)==(#Y) #end JScript comment */
WScript.StdErr.Write("Enter a number:");
WScript.StdIn.Read(0);
var strMyName = WScript.StdIn.ReadLine();
var num=parseInt(strMyName);
while(isNaN(num)){
WScript.StdErr.Write("Enter a number:");
WScript.StdIn.Read(0);
var strMyName = WScript.StdIn.ReadLine();
var num=parseInt(strMyName);
}
WScript.StdOut.WriteLine(num);

You can't use spaces around == comparisions. Your code says if %NewPenaltyLimit%<space>==<space>Numbers?? which will never match.

Related

Only accept numeric characters in batch file input

I am making a game from a batch file and one of the inputs can accept any character (~!##$%^&*()`) and any other. Is there any way to look for any character other than numbers and use the GOTO command? This is my script so far:
set /p guess=
echo "%guess%"|findstr /L "[a-z][A-Z]~`!##$%^&*()-_=+\^|^^;:"',<.>/?*"
if %errorlevel% == 0 goto Invalid_Number
if %guess% == %number% goto Correct
... everything else here ...
:Invalid_Number
echo Invalid Number. Input must be a number
pause
Is there any way to make this work, all it says is Access Denied, I am testing this on a school computer though, it might not work.
Put this at the bottom of your script:
:isInt <str>
for /f "delims=0123456789" %%a in ("%1") do exit /b 1
exit /b 0
Then to invoke it, do
call :isInt %guess% && success || fail
Here's a more complete example:
#echo off
setlocal
set /a rand = %RANDOM% %% 10 + 1
:begin
set /P "guess=Guess a number between 1 and 10: "
call :isInt %guess% || goto invalid
if %guess% gtr 0 if %guess% lss 11 (
if %guess% equ %rand% (
echo Lucky guess!
exit /b
) else (
echo Oooh, so close. Try again.
goto begin
)
)
:invalid
echo Please enter a valid integer between 1 and 10.
goto begin
:isInt <str>
for /f "delims=0123456789" %%a in ("%1") do exit /b 1
exit /b 0
This is the same basic idea as MC ND's solution, but instead of using the for statement to unset %guess%, it sets %errorlevel% and stops looping at the first non-numeric character. This makes it infinitesimally more efficient. :)
And with either success or fail, I like to use conditional execution (the && and || stuff).
:ask
set /p "guess=?" || goto :ask
setlocal enabledelayedexpansion
for /f "delims=0123456789" %%a in ("!guess!") do set "guess="
endlocal & set "guess=%guess%"
if not defined guess (
echo invalid input
goto ask
)
echo valid input
The basic idea behind the test is to use the numbers as delimiters in a for /f command, so they are removed from the input. If anything remains it is not a number and the code in the do clause is executed.
The delayedexpansion is enabled/disabled to handle problematic characters (specially double quotes) that could be typed in the input field.
May I suggest you a different, better approach? Instead of read any line and then check if it contains a number, your program may directly read a number, so the checking is not necessary. The way to do that is emulating SET /P command via a subroutine. This way, you may add additional constraints to the input, like read a maximum number of digits, for example.
#echo off
rem Read a number emulating SET /P command
rem Antonio Perez Ayala
setlocal
rem Define the following variable before call InputNumber subroutine
set "thisFile=%~F0"
call :InputNumber number="Enter a number of up to 5 digits: " 5
echo Number read: %number%
goto :EOF
:InputNumber var="prompt" [digits]
setlocal EnableDelayedExpansion
rem Initialize variables
if "%~3" equ "" (set numDigits=9) else set "numDigits=%3"
set "digits=0123456789"
for /F %%a in ('copy /Z "%thisFile%" 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 "%thisFile%" "%thisFile%" 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 If key is not a digit: ignore it
if "!digits:%key%=!" equ "%digits%" goto nextKey
rem If can not accept more digits: ignore it
if %i% equ %numDigits% goto nextKey
rem Else: show and accept the digit
set /P "=%key%" < NUL
set "input=%input%%key%"
set /A i+=1
goto nextKey
:endRead
echo/
endlocal & set "%~1=%input%"
exit /B
You may also add any other processing to the input line, like show asterisks instead of digits, etc. For a large example on this topic, see this post

How to check a string does not start with a number in Batch?

How can I check that the first character of a string is a letter and so that it is not a number, or rather a cipher? There are no spaces or special characters in this string.
#ECHO OFF
SETLOCAL
SET /a num=5678
CALL :initnum
SET "num=hello"
CALL :initnum
SET "num=4ello"
CALL :initnum
SET "num=hell0"
CALL :initnum
SET "num=he8lo"
CALL :initnum
SET "num="
CALL :initnum
ECHO(==============
SET /a nam=7654
SET "nem=hello"
SET "nim=4ello"
SET "nom=hell0"
SET "num=he8lo"
SET "nzm="
CALL :initnum2 nam
CALL :initnum2 nem
CALL :initnum2 nim
CALL :initnum2 nom
CALL :initnum2 num
CALL :initnum2 nzm
GOTO :EOF
:initnum
IF NOT DEFINED num ECHO NUM is empty, so it doesn't begin with a numeric&GOTO :EOF
FOR /l %%a IN (0,1,9) DO IF %num:~0,1%==%%a ECHO %num% Begins with numeric&GOTO :EOF
ECHO %num% Does NOT begin with a numeric
GOTO :eof
:initnum2
IF NOT DEFINED %1 ECHO %1 is empty, so it doesn't begin with a numeric&GOTO :EOF
CALL SET "$1=%%%1%%"
FOR /l %%a IN (0,1,9) DO IF %$1:~0,1%==%%a ECHO %1 (%$1%) Begins with numeric&GOTO :EOF
ECHO %1 (%$1%) Does NOT begin with a numeric
GOTO :eof
You should be able to get what you want from this demo.
#echo off
setlocal enableextensions disabledelayedexpansion
set "var=1hello"
for /f "tokens=* delims=0123456789" %%a in ("%var%") do (
if not "%%a"=="%var%" echo var starts with a number
)
If the var contents starts with a number, the token/delim management in the for command will remove it.
edited just to include the usual (included the previous code) and some less used options just in case someone is interested
#echo off
setlocal enableextensions disabledelayedexpansion
set "var=1hello"
echo(%var%
rem Option 1 - Use the for command to tokenize the string
rem A dot is added to handle empty vars
for /f "tokens=* delims=0123456789" %%a in ("%var%.") do (
if not "%%a"=="%var%." (
echo var starts with a number
) else (
echo var does not start with a number
)
)
rem Option 2 - Use set arithmetic and detect errors
rem This will fail if the string starts with + or -
set "%var%_=0"
set /a "test=%var%_" 2>nul
if not errorlevel 1 (
echo var does not start with a number
) else (
echo var starts with a number
)
rem Option 3 - Use substring operations and logic operators
set "test=%var%."
if "%test:~0,1%" GEQ "0" if "%test:~0,1%" LEQ "9" set "test="
if defined test (
echo var does not start with a number
) else (
echo var starts with a number
)
rem Option 4 - Use findstr
rem This is SLOW as findstr needs to be executed
echo(%var%|findstr /b /r /c:"[0-9]" >nul && (
echo var starts with a number
) || (
echo var does not start with a number
)
I think this is the simplest way:
#echo off
setlocal EnableDelayedExpansion
set digits=0123456789
set var=1something
if "!digits:%var:~0,1%=!" neq "%digits%" (
echo First char is digit
) else (
echo First char is not digit
)
The first character of var is tried to be removed from digits string. If such a char was a digit, digits string change; otherwise, digits string remains the same.
#echo off
setlocal
set "the_string=a23something"
for /l %%a in (%the_string% ; 1 ; %the_string%) do set "cl_string=%%~a"
if %the_string:~0,1% neq 0 if "%cl_string%" equ "0" (
echo does not start with number
) else (
echo starts with number
)
endlocal
Another approach is with FINDSTR which eventually will be slower as it is an external for cmd.exe command.
#echo off
set "the_string=something"
echo %the_string%|findstr /b /r "[0-9]" >nul 2>&1 && (
echo starts with number
) || (
echo does not start with number
)
This will work in your situation:
echo %variable%|findstr "^[a-zA-Z]" >nul && echo it starts with an alpha character
Using findstr with regexp :
#echo off
set "$string=2toto"
echo %$string:~0,1%|findstr /i "^-*0*x*[0-9][0-9]*$">nul && echo is NUM || echo Is not NUM
in place of echo is NUM or echo is not NUM you can use a goto to redirect your script the way you want it.
#echo off
set "$string=2toto"
echo %$string:~0,1%|findstr /i "^-*0*x*[0-9][0-9]*$">nul && goto:isnum || goto:isnotnum
:isnum
echo is NUM
exit/b
:isnotnum
echo is not NUM
You have to set the string as a variable; in this way you are able to extract substrings from a main string. Here is an example:
#echo off
set EXAMPLESTRING=12345abcde
set FIRSTCHARACTERSTRING=%EXAMPLESTRING:~0,1%
The result of this short script should be 1 in this case.
Then, you can set a series of conditions to verify whether the first character is a number or not:
if %FIRSTCHARACTERSTRING%==0 goto NUMBER
if %FIRSTCHARACTERSTRING%==1 goto NUMBER
if %FIRSTCHARACTERSTRING%==2 goto NUMBER
if %FIRSTCHARACTERSTRING%==3 goto NUMBER
if %FIRSTCHARACTERSTRING%==4 goto NUMBER
if %FIRSTCHARACTERSTRING%==5 goto NUMBER
if %FIRSTCHARACTERSTRING%==6 goto NUMBER
if %FIRSTCHARACTERSTRING%==7 goto NUMBER
if %FIRSTCHARACTERSTRING%==8 goto NUMBER
if %FIRSTCHARACTERSTRING%==9 goto NUMBER
goto LETTER
:NUMBER
echo The first character is a number!
goto EOF
:LETTER
echo The first character is a letter!
goto EOF
Maybe this is not the most efficient solution but it works fine and it is easier to understand.

Removing non alphanumeric characters in a batch variable

In batch, how would I remove all non alphanumeric (a-z,A-Z,0-9,_) characters from a variable?
I'm pretty sure I need to use findstr and a regex.
The solutionof MC ND works, but it's really slow (Needs ~1second for the small test sample).
This is caused by the echo "!_buf!"|findstr ... construct, as for each character the pipe creates two instances of cmd.exe and starts findstr.
But this can be solved also with pure batch.
Each character is tested if it is in the map variable
:test
set "_input=Th""i\s&& is not good _maybe_???"
set "_output="
set "map=abcdefghijklmnopqrstuvwxyz 1234567890"
:loop
if not defined _input goto endLoop
for /F "delims=*~ eol=*" %%C in ("!_input:~0,1!") do (
if "!map:%%C=!" NEQ "!map!" set "_output=!_output!%%C"
)
set "_input=!_input:~1!"
goto loop
:endLoop
echo(!_output!
And it could be speed up when the goto loop is removed.
Then you need to calculate the stringLength first and iterate then with a FOR/L loop over each character.
This solution is ~6 times faster than the above method and ~40 times faster than the solution of MC ND
set "_input=Th""i\s&& is not good _maybe_!~*???"
set "_output="
set "map=abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890"
%$strLen% len _input
for /L %%n in (0 1 %len%) DO (
for /F "delims=*~ eol=*" %%C in ("!_input:~%%n,1!") do (
if "!map:%%C=!" NEQ "!map!" set "_output=!_output!%%C"
)
)
exit /b
The macro $strlen can be defined with
set LF=^
::Above 2 blank lines are required - do not remove
#set ^"\n=^^^%LF%%LF%^%LF%%LF%^^":::: StrLen pResult pString
set $strLen=for /L %%n in (1 1 2) do if %%n==2 (%\n%
for /F "tokens=1,2 delims=, " %%1 in ("!argv!") do (%\n%
set "str=A!%%~2!"%\n%
set "len=0"%\n%
for /l %%A in (12,-1,0) do (%\n%
set /a "len|=1<<%%A"%\n%
for %%B in (!len!) do if "!str:~%%B,1!"=="" set /a "len&=~1<<%%A"%\n%
)%\n%
for %%v in (!len!) do endlocal^&if "%%~b" neq "" (set "%%~1=%%v") else echo %%v%\n%
) %\n%
) ELSE setlocal enableDelayedExpansion ^& set argv=,
EDITED - #jeb is right. This works but is really, really slow.
#echo off
setlocal enableextensions enabledelayedexpansion
set "_input=Th""i\s&& is not good _maybe_???"
set "_output="
:loop
if not defined _input goto endLoop
set "_buf=!_input:~0,1!"
set "_input=!_input:~1!"
echo "!_buf!"|findstr /i /r /c:"[a-z 0-9_]" > nul && set "_output=!_output!!_buf!"
goto loop
:endLoop
echo !_output!
endlocal
So, back to the drawing board. How to make it faster? lets try to do as less operations as we can and use as much long substring as we can. So, do it in two steps
1.- Remove all bad characters that can generate problems. To do it we will use the hability of for command to identify these chars as delimiters , and then join the rest of the sections of god characters of string
2.- Remove the rest of the bad characters, locating them in string using the valids charactes as delimiters to find substrings of bad characters, replacing then in string
So, we end with (sintax adapted to what has been answered here)
#echo off
setlocal enableextensions enabledelayedexpansion
rem Test empty string
call :doClean "" output
echo "%output%"
rem Test mixed strings
call :doClean "~~asd123#()%%%^"^!^"~~~^"""":^!!!!=asd^>^<bm_1" output
echo %output%
call :doClean "Thi\s&& is ;;;;not ^^good _maybe_!~*???" output
echo %output%
rem Test clean string
call :doClean "This is already clean" output
echo %output%
rem Test all bad string
call :doClean "*******//////\\\\\\\()()()()" output
echo "%output%"
rem Test long string
set "zz=Thi\s&& is not ^^good _maybe_!~*??? "
set "zz=TEST: %zz%%zz%%zz%%zz%%zz%%zz%%zz%%zz%%zz%%zz%%zz%%zz%%zz%%zz%%zz%%zz%%zz%%zz%%zz%%zz%"
call :doClean "%zz% TEST" output
echo %output%
rem Time long string
echo %time%
for /l %%# in (1 1 100) do call :doClean "%zz%" output
echo %time%
exit /b
rem ---------------------------------------------------------------------------
:doClean input output
setlocal enableextensions enabledelayedexpansion
set "map=abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 "
set "input=%~1"
set "output="
rem Step 1 - Remove critical delimiters
(
:purgeCritical
for /L %%z in (1 1 10) do (
for /f tokens^=1^-9^,^*^ delims^=^=^"^"^~^;^,^&^*^%%^:^!^(^)^<^>^^ %%a in ("!input!") do (
set "output=!output!%%a%%b%%c%%d%%e%%f%%g%%h%%i"
set "input=%%j"
)
if not defined input goto outPurgeCritical
)
goto purgeCritical
)
:outPurgeCritical
rem Step 2 - remove any remaining special character
(
:purgeNormal
for /L %%z in (1 1 10) do (
set "pending="
for /f "tokens=1,* delims=%map%" %%a in ("!output!") do (
set "output=!output:%%a=!"
set "pending=%%b"
)
if not defined pending goto outPurgeNormal
)
goto purgeNormal
)
:outPurgeNormal
endlocal & set "%~2=%output%"
goto :EOF
Maybe not the fastest, but at least a "decent" solution
#echo eof
call :purge "~~asd123#()%%%^"^!^"~~~^:^=asd^>^<bm_1" var
echo (%var%)
goto :eof
:purge StrVar [RtnVar]
setlocal disableDelayedExpansion
set "str1=%~1"
setlocal enableDelayedExpansion
for %%a in ( - ! # # $ % ^^ ^& + \ / ^< ^> . ' [ ] { } ` ^| ^" ) do (
set "str1=!str1:%%a=!"
)
rem dealing with some delimiters
set "str1=!str1:(=!"
set "str1=!str1:)=!"
set "str1=!str1:;=!"
set "str1=!str1:,=!"
set "str1=!str1:^^=!"
set "str1=!str1:^~=!"
set "temp_str="
for %%e in (%str1%) do (
set "temp_str=!temp_str!%%e"
)
endlocal & set "str1=%temp_str%"
setlocal disableDelayedExpansion
set "str1=%str1:!=%"
set "str1=%str1::=%"
set "str1=%str1:^^~=%"
for /f "tokens=* delims=~" %%w in ("%str1%") do set "str1=%%w"
endlocal & set "str1=%str1%"
endlocal & if "%~2" neq "" (set %~2=%str1%) else echo %str1%
goto :eof
Still cannot deal with ~ and = but working on it
EDIT: = now will be cleared
EDIT: ~ now will be cleared

Read each character of argument in a batch file

sample input in cmd:
test.bat /p 1,3,4
expected result:
1
3
4
my codes so far:
#echo off
set arg = %1
set var = %2
if (%1)==(/p) (
... need code that will read and print each character of var
)
There is a potential problem with your question. If test.bat is:
#echo %1%
Then
test 1,2,3
Prints:
1
Because, in this context, the comma is treated as an argument delimiter.
So you either need to enclose in quotes:
test "1,2,3"
Or use a different internal delimiter:
test 1:2:3
Unless you want the parts to be placed in %2, %3, etc., in which case you problem is solved by a trivial use of SHIFT.
For my solution I have elected to require quotes around the group parameter, "1,2,3" (though this is easily adapted for a different delimiter by changing delims=, to specify the character you want to use).
#echo off
setlocal ENABLEDELAYEDEXPANSION ENABLEEXTENSIONS
set args=%~2
if "%1"=="/p" (
:NextToken
for /F "usebackq tokens=1* delims=," %%f in ('!args!') do (
echo %%f
set args=%%g
)
if defined args goto NextToken
)
Call like:
readch.bat /p "1,2,3"
%~2 is used to remove the quotes.
The FOR statement parses args, puts the first token in %f and the remainder of the line in %g.
The `goto NextToken' line loops until there are no more tokens.
#echo off
if "%1"=="/p" (
:LOOP
echo %2
shift
set arg=%2
if defined arg goto :LOOP else exit >nul
)
set params=%%~2
if (%1)==(/p) (
set params=%params:,= %
)
if "%params%" NEQ "" (
call :printer %params%
)
goto :eof
:printer
:shifting
if "%%1" NEQ "" (
echo %%1
) else (
goto :eof
)
shift
goto :shifting
goto :eof

How to receive even the strangest command line parameters?

as discussed in an other thread How to avoid cmd.exe interpreting shell special characters like < > ^
it is not easy to get all parameters from the command line.
A simple
set var=%1
set "var=%~1"
are not enough, if you have a request like
myBatch.bat abc"&"^&def
I have one solution, but it needs a temporary file, and it is also not bullet proof.
#echo off
setlocal DisableDelayedExpansion
set "prompt=X"
(
#echo on
for %%a in (4) do (
rem #%1#
)
) > XY.txt
#echo off
for /F "delims=" %%a in (xy.txt) DO (
set "param=%%a"
)
setlocal EnableDelayedExpansion
set param=!param:~7,-4!
echo param='!param!'
It fails with something like myBatch.bat %a, it display 4 not the %a
in this situation a simple echo %1 would work.
It's obviously the for-loop but I don't know how to change this.
Perhaps there exists another simple solution.
I don't need this to solve an actual problem, but I like solutions that are bullet proof in each situation, not only in the most cases.
I don't think anyone found any holes in this, except for the inability to read newlines in the parameters:
#echo off
setlocal enableDelayedExpansion
set argCnt=1
:getArgs
>"%temp%\getArg.txt" <"%temp%\getArg.txt" (
setlocal disableExtensions
set prompt=#
echo on
for %%a in (%%a) do rem . %1.
echo off
endlocal
set /p "arg%argCnt%="
set /p "arg%argCnt%="
set "arg%argCnt%=!arg%argCnt%:~7,-2!"
if defined arg%argCnt% (
set /a argCnt+=1
shift /1
goto :getArgs
) else set /a argCnt-=1
)
del "%temp%\getArg.txt"
set arg
The above comes from a lively DosTips discussion - http://www.dostips.com/forum/viewtopic.php?p=13002#p13002. DosTips user Liviu came up with the critical SETLOCAL DisableExtensions piece.
The code below is based on the rambling Foolproof Counting of Arguments topic on DosTips and this answer by jeb:
#echo off & setLocal enableExtensions disableDelayedExpansion
(call;) %= sets errorLevel to 0 =%
:: initialise variables
set "paramC=0" & set "pFile=%tmp%\param.tmp"
:loop - the main loop
:: inc param counter and reset var storing nth param
set /a paramC+=1 & set "pN="
:: ECHO is turned on, %1 is expanded inside REM, GOTO jumps over REM,
:: and the output is redirected to param file
for %%A in (%%A) do (
setLocal disableExtensions
set prompt=#
echo on
for %%B in (%%B) do (
#goto skip
rem # %1 #
) %= for B =%
:skip - do not re-use this label
#echo off
endLocal
) >"%pFile%" %= for A =%
:: count lines in param file
for /f %%A in ('
find /c /v "" ^<"%pFile%"
') do if %%A neq 5 (
>&2 echo(multiline parameter values not supported & goto die
) %= if =%
:: extract and trim param value
for /f "useBack skip=3 delims=" %%A in ("%pFile%") do (
if not defined pN set "pN=%%A"
) %= for /f =%
set "pN=%pN:~7,-3%"
:: die if param value is " or "", else trim leading/trailing quotes
if defined pN (
setLocal enableDelayedExpansion
(call) %= OR emulation =%
if !pN!==^" (call;)
if !pN!=="" (call;)
if errorLevel 1 (
for /f delims^=^ eol^= %%A in ("!pN!") do (
endLocal & set "pN=%%~A"
) %= for /f =%
) else (
>&2 echo(empty parameter values (""^) not supported & goto die
) %= if errorLevel =%
) else (
:: no more params on cmd line
set /a paramC-=1 & goto last
) %= if defined =%
:: die if param value contains "
if not "%pN:"=""%"=="%pN:"=%" (
>&2 echo(quotes (^"^) in parameter values not supported & goto die
) %= if =%
:: assign nth param, shift params, and return to start of loop
set "param%paramC%=%pN%" & shift /1 & goto loop
:last - reached end of params
:: no param values on cmd line
if %paramC% equ 0 (
>&2 echo(no parameter values found & goto die
) %= if =%
:: list params
set param
goto end
:die
(call) %= sets errorLevel to 1 =%
:end
:: exit with appropriate errorLevel
endLocal & goto :EOF
The following conditions will terminate the program immediately:
no parameters found
multiline parameter
empty parameter (""", or " is permitted for the last parameter)
one or more quotes (") in a parameter value
To ease these restrictions, simply comment out the relevant lines. Read the inline comments for more information. Do not attempt to turn off the multiline parameter trap!
I invented the syntax-error-technic to solve the problem (partially).
With this solution it's even possible to receive multiline parameters and also carriage return characters.
There is no known parameter which fails!
BUT the drawback of this solution, the main process exits and only a child process continues.
That is a consequence of the capture trick, a syntax error is created by using an invalid parenthesis block ( Prepare ) PARAMS....
But the syntax error itself outputs the complete block, including the expanded value of %*.
The output is redirected to a file by the permanent redirect technic.
And the child process can retrieve the complete parameter from the file.
This solution can be useful, when the batch file only handles the parameter and always exit afterwards.
#echo off
REM *** Thread redirector
for /F "tokens=3 delims=:" %%F in ("%~0") do goto %%F
REM *** Clear params.tmp
break > params.tmp
start "" /b cmd /k "%~d0\:StayAlive:\..\%~pnx0 params.tmp"
(set LF=^
%=empty=%
)
REM *** Change prompt for better recognition
prompt #PROMPT#
REM *** Change streams permanently
REM *** stream1 redirects to params.tmp
REM *** stream2 redirects to nul
echo on >nul 2>nul 0>nul 3>params.tmp 4>nul 5>&3
#REM *** This is the magic part, it forces a syntax error, the error message itself shows the expanded %asterix without ANY modification
( Prepare ) PARAMS:%LF%%*%LF%
echo Works
exit /b
REM *** Second thread to fetch and show the parameters
:StayAlive
:__WaitForParams
if %~z1 EQU 0 (
goto :__WaitForParams
)
REM *** Show the result
findstr /n "^" %1
It's up to the user who types the command to escape any special characters. Your program cannot do anything about what the shell does before your program even runs. There is no other "bullet proof" solution to this.

Resources