IF gives the wrong answer when I try to compare 2 large numbers.
For example, this simple batch file
#echo off
setlocal
set n1=30000000000000
set n2=40000000000
if %n1% gtr %n2% echo %n1% is greater than %n2%
if %n1% lss %n2% echo %n1% is less than %n2%
if %n1% equ %n2% echo %n1% is equal to %n2%
produces
30000000000000 is equal to 40000000000
What is going on, and how do I fix this?
If both sides of an IF comparison are composed strictly of decimal digits, then IF will interpret both sides as numbers. This is what enables IF to correctly determine that 10 is greater than 9. If you have any non digit characters, then IF does a string comparison. For example, "10" is less than "9" because the quotes are not digits, and 1 sorts lower than 9.
The reason the comparison in the question fails is because CMD.EXE cannot process numbers larger than 2147483647. An odd design quirk in IF treats any number larger than 2147483647 as being equal to 2147483647.
If you want to do a string comparison of large numbers, then the solution is easy. You just need to add 1 or more non digit characters to both sides of the condition. The following script -
#echo off
setlocal
set n1=30000000000000
set n2=40000000000
if "%n1%" gtr "%n2%" echo "%n1%" is greater than "%n2%"
if "%n1%" lss "%n2%" echo "%n1%" is less than "%n2%"
if "%n1%" equ "%n2%" echo "%n1%" is equal to "%n2%"
produces the correct string comparison result
"30000000000000" is less than "40000000000"
But in most cases, this is not what is wanted.
If you want to do a numeric comparison, then the process is a bit more involved. You need to convert the number into a string that will sort properly as a number. This is accomplished by prefixing the numeric string with zeros in a way that makes both numeric strings the same width. The simplest solution is to determine the maximum number of digits you need to support - let's say 15 for this example. So you prefix each value with 15 zeros, and then preserve only the right-most 15 characters by using a substring operation. You also need to add a non-digit to both sides as before - again quotes work well.
This script -
#echo off
setlocal
set n1=30000000000000
set n2=40000000000
call :padNum n1
call :padNum n2
if "%n1%" gtr "%n2%" echo %n1% is greater than %n2%
if "%n1%" lss "%n2%" echo %n1% is less than %n2%
if "%n1%" equ "%n2%" echo %n1% is equal to %n2%
exit /b
:padNum
setlocal enableDelayedExpansion
set "n=000000000000000!%~1!"
set "n=!n:~-15!"
endlocal & set "%~1=%n%"
exit /b
produces -
030000000000000 is greater than 000040000000000
Note that left prefixing with spaces works just as well as zeros.
You can later remove the leading zeros whenever you want using the following (or adapt to remove leading spaces)
for /f "tokens=* delims=0" %%A in ("%n1%") do set "n1=%%A"
if not defined n1 set "n1=0"
Normally we don't deal with large numbers in batch files. But they can easily crop up if we look at free space on a hard disk. Terabyte disk drives are now relatively inexpensive. This is how I first ran into comparison of large numbers at https://stackoverflow.com/a/9099542/1012053
I chose to support 15 digits in my example because that equates to almost 999 terabytes. I imagine it will be a while before we have to deal with disk drives larger than that. (But who knows!)
EDIT - My description of how IF parses numbers is intentionally overly simplistic. IF actually supports negative numbers, as well as hex and octal notation. See Rules for how CMD.EXE parses numbers for a much more thorough explanation.
Related
Hi everyone I writing little .bat file and seemed for I was ended but I got error. Operator IF does not checking Permission value and Free space conditions of values and are not compering. All the time I get "false" even when the free space on disc D is less then 25Gb (26,843,545,600 bits)
My code looks such:
#echo off
#setlocal enableextensions enabledelayedexpansion
set permissiblevalue=26,843,545,600
set permissiblevalue=%permissiblevalue:,=%
for /f "tokens=3" %%a in ('dir d:\') do (
set bytesfree=%%a
)
set bytesfree=%bytesfree:,=%
if %permissiblevalue% leq %bytesfree% (
The disk D is checking.....
) else (
echo msgbox "Lacks of free space on disk D. Klick OK for delete files!!!" > %tmp%\tmp.vbs
wscript %tmp%\tmp.vbs
del %tmp%\tmp.vbs
%SystemRoot%\explorer.exe "d:\"
)
exit
Please explain to me Where is the mistake
If the two arguments to if are both pure numeric strings, cmd will convert them to integers and compare them as integers. BUT cmd is limited to 2^31 and mechanically processes the string character-by-character, left-to-right, multiplying by 10 and adding the next value, so any value greater than 2147483647 will be processed incorrectly (but won't generate en error).
Consequently, if you are using large numbers, you need to force cmd to interpret them as strings. This is easily done by "quoting each string".
BUT a string comparison is performed character-by-character, so you need to leading-0-fill each so that they are the same length.
To do this :
set "zeroes=000000000000000000000000000000000000000000000000000000000"
set "var=%zeroes%%var%"
set "var=%var:~-20%"
Which prepends a series of 0 characters to the current value of var, then sets var to the last 20 characters of the result.
You can then safely use if "%var%" leq "%someothervar%"
Tip : Use set "var=value" for setting string values - this avoids problems caused by trailing spaces. Don't assign a terminal \, Space or " - build pathnames from the elements - counterintuitively, it is likely to make the process easier.
I'm writing a batch script where I need to extract numbers from a string (which indicates the version of a file), so that I can compare it with another number.
Below is my script written so far
:: Over here I'm trying to extract number from the string , this isn't working
for %%F in ("!name!\.." ) do (
::set "number=%%~nxF" |findstr /b /e /r "\"[0-9]*\""
set res=%name:findstr /r "^[1-9][0-9]*$"
echo !res!
)
In the last two for loop I have implemented the extract number logic, but it just prints The system cannot find the drive specified.
If anybody could help me with this issue that would be a great help.
I have a batch file that asks for input, stores this input in a var and then uses the var in a ping. I need to make sure that the input matches one of several naming conventions
Naming conventions:
PCX1 can be as high as 100
GENPRT1 can be as high as 100
NETPRT1 can be as high as 100
FAXPRT1 can be as high as 100
So if i enter 12 it will not work but if I enter PCX12 it will.
Everything in the script works except the regex. How can i get this to work?
if "%sta%" == "findstr %sta% ^PCX[0-9]*[0-9]*[0-9]$ \i" (
echo The syntax is correct
goto PING
) else (
set errmsg=The syntax is wrong
goto START
)
This should help:
^(PCX|GENPRT|NETPRT|FAXPRT)([\d]|0[\d]|00[\d]|0[\d][\d]|[\d][\d]|100)$
FINDSTR's regex flavor is extremely limited. It doesn't even support alternation (|), so even very simple problems are going to have very messy solutions. Here's the most compact expression I can come up with:
FINDSTR /R /I "^PCX[1-9][0-9]?$ ^PCX100$ ^GENPRT[1-9][0-9]?$ ^GENPRT100$ ^NETPRT[1-9][0-9]?$ ^NETPRT100$ ^FAXPRT[1-9][0-9]?$ ^FAXPRT100$"
Each space-separated sequence is treated as a separate regex, so this tries to perform up to eight matches on each string it tests. That's not to say it's slow, but it's a pain in the butt to use when you're used to real regexes.
For reference, here's how I would have written that in a serous regex flavor:
^(PCX|((GEN|NET|FAX)PRT))([1-9][0-9]?|100)$
If you have the option of using a different tool (like PowerShell, which uses .NET's very powerful and feature-rich regex flavor), I strongly recommend you do so.
#echo off
setlocal disabledelayedexpansion
:start
set /p "sta=What ? "
cmd /v /d /q /c "(echo(!sta!)" ^
| findstr /i /r /b /e "PCX[0-9]* GENPRT[0-9]* NETPRT[0-9]* FAXPRT[0-9]*" ^
| findstr /r /e "[^0-9][1-9] [^0-9][1-9][0-9] [^0-9]100" > nul
if errorlevel 1 (
echo The syntax is wrong
goto :start
)
echo The syntax is correct
A new cmd instance is used to ensure the tested string will not include any parser added space at the end. The output of the echo command is tested to see if it matches any of the starting strings followed by numbers up to the end. Then it is tested again for a valid number range.
If errorlevel is set, the value does not match the condition and a new value is requested.
If errorlevel is not set, the value is correct.
I have a situation where I need to combine four CSV files into one CSV file. This is easy if i just wanted to add them one after the other, but I need to get them to be side by side in the CSV file. I know that all four files have the same number of entries (in the 1000 entry range). I have been working on the following code which works for small files but is extremely inefficient for long files. Is there any easier way to accomplish the same task?
#echo off
setlocal enabledelayedexpansion
Set R=1
Set T=1
Set Y=1
Set U=1
for /f %%a in (test1.txt) do (
set I=!R!
for /f %%b in (test2.txt) do (
set J=!T!
for /f %%c in (test3.txt) do (
set K=!Y!
for /f %%d in (test4.txt) do (
set L=!U!
If !I!==!J! If !J!==!K! If !K!==!L! echo %%a,%%b,%%c,%%d >> TestComplete.txt
Set /a U=U+1
)
Set U=1
Set /a Y=Y+1
)
Set Y=1
Set /a T=T+1
)
Set T=1
Set /a R=R+1
)
Note: I know that the code I have pasted in is using .txt files and not .csv files. I assume what will work for one will work for the other.
Again, The above code seems to work great as long as the files are small. I have trouble (of course) when the files (in this case test1.txt) have around 1000 lines of text.
GnuWin, a collection of ports of GNU (Unix like) tools to Windows, contains paste which does exactly what you want. However, I sense you'd prefer a native Windows answer and I've come up with one, but it's not pretty. Starting with #walid2mi's clever solution and striping it down to coljoin.bat:
#echo off
setlocal enabledelayedexpansion
for /f "delims=" %%a in (%1) do (
set /p line=
echo !line!, %%a
)
You can join four files with:
type a.txt | coljoin b.txt | coljoin c.txt | coljoin d.txt > TestComplete.txt
Time required should increase linearly with file length, avoiding the drastic slowdown of your solution. However the failure mode of this solution is not good when all the files do not have an equal number of lines. The user will usually be prompted to enter missing lines.
I am trying to understand a batch file that was sent to me in order to work around a bug in a third party program while they resolve the issue. Basically they are running a findstr regular expression command in order to determine whether or not the string matches. If it does, then the special characters that should not be stripped out are being added back in manually before it is passed off to the original commandline program.
As best I can tell though, what has been provided does not work or I do not understand it. I am pasting the relevant section of code below.
#echo off
setlocal
set username=%1
shift
echo %username% | findstr /r "^\"[0-9][0-9]*\"" >nul
if not errorlevel 1 (set username=";%username:~0,9%=%username:~10,4%?")
echo %username%
The three pieces I really have questions about are as follows:
I believe the unescaped interpretation of the regular express above is ^"[0-9][0-9]*" which I think means that the string must begin with a numeric character and then must consist of zero or more additional numeric-only characters in order for a match to be found. Well, FINDSTR seems to be doing something weird with the escaped quotes and I cannot get it to match anything I have tried. If I remove the \" around [0-9][0-9]* then I can get it to work, but it does not properly reject non-numeric characters such as an input string of 123456789O1234 (there is a letter O instead of a zero in that sample string).
What is the point of the >nul
Wouldn't it be better to check for an errorlevel equal to 0 instead of "not errorlevel 1" since it could possibly return an error level of 2?
Anyway, the following code works, but it is not as precise as I would like. I am just looking to understand why the quotes in the regex string are not working. Perhaps this is a limitation of FINDSTR, but I have not came across anything definitive yet.
#echo off
setlocal
set username=%1
shift
echo %username% | findstr /r "^[0-9][0-9]*" >nul
if not errorlevel 1 (set username=";%username:~0,9%=%username:~10,4%?")
echo %username%
I can workaround the problem by repeating the class 14 times since that is the number of characters in my situation (more than 15 classes will cause it to crash - scroll to the bottom). I am still curious as to how this could be achieved more simply, and of course the remaining 2 questions.
EDIT / WORKING SOLUTION
#echo off
setlocal enableDelayedExpansion
set username=%~1
shift
echo !username!|findstr /r /c:"^[0-9][0-9]*$" >nul
if not errorlevel 1 (set username=";!username:~0,9!=!username:~10,4!?")
echo !username!
NOTES:
When I first ran it after modifying my existing code to more cloesly resemble dbenham's, enableDelayedExpansion gave an error as did the quotes around setting the username (see below). I can't replicate what I did wrong, but it is all working now (this is in case someone else comes across the same issue).
I had tried the $ for the EOL marker (which is the key to forcing it match numeric content only), but I think that the other problems were getting in the way which made me think it was not the solution. Also, to ensure the $ works don't miss this part of dbenham's answer "...you must also make sure there are no spaces between your echoed value and the pipe symbol."
In short it pretty much seems that trying to put double quotes inside a regex for findstr is wrong syntax/does not work/etc... unless you are actually looking to match " in the string/files you are parsing through. See dbenham's answer for clarity here. As he noted, you can use %~1 to strip the quotes from the argument instead of adding it to your regex (and programmatically add them back in if needed).
Error Message
C:>sample.bat 123456789
'enableDelayedExpansion' is not recognized as an internal or external command,
operable program or batch file.
'"' is not recognized as an internal or external command,
operable program or batch file.
!username!
Reference Links:
Undocumented features and limitations of the Windows FINDSTR command
Case sesntive anomalies with findstr (not handling case properly in some circumstances)
http://ss64.com/nt/findstr.html
http://www.robvanderwoude.com/findstr.php
http://www.microsoft.com/resources/documentation/windows/xp/all/proddocs/en-us/findstr.mspx
Answering your questions in reverse order:
3) if not errorlevel 1 is probably the same as if %errorlevel%==0 because IF ERRORLEVEL 1 means if ERRORLEVEL is greater than or equal to 1. So putting a NOT in front means if ERRORLEVEL is less than 1. I believe FINDSTR never returns a negative ERRORLEVEL, so the syntax should be OK.
2) The >nul redirects the stdout output of FINDSTR to the nul device, meaning it disables the output. Normally any matching line would be printed. You are only interested in the return code - you don't want to see the output.
1) The original regex will match any input string that starts with a quote, followed by at least one digit, followed by another quote. It ignores any characters that may appear after the 2nd quote.
So the following strings (quotes included) will match:
"0"
"01234"
"0"a
"01234"a
The following strings will not match:
0
01234
""
"0a"
The original code has problems if the number of digits in the matching string reaches a certain length because the ending quote gets stripped causing the closing ) to be quoted and so the rest of the script fails.
I don't understand your requirements so I don't know how to fix the code.
It sounds like you don't want to match strings that have non digits. That means you need to include the end of line marker $ at the end of the regex. But you must also make sure there are no spaces between your echoed value and the pipe symbol.
I believe you probably don't want quotes in your value, (or else you should programatically add them at the very end). You can use %~1 to strip any enclosing quotes from the supplied argument.
If you are looking to check if argument 1 consists of nothing but numeric digits, then you can use:
setlocal enableDelayedExpansion
set "username=%~1"
echo !username!|findstr /r "^[0-9][0-9]*$" >nul
I used delayed expansion because you have no control over what characters are in %1, and if it contains special characters like & or | it will cause problems if you use normal expansion. The syntax I have given is not bullet proof, but it handles most "normal" situations.
It is not necessary in your case, but I prefer to use the /c option, just in case your search string contains spaces. So the above could be written as
echo !username!|findstr /r /c:"^[0-9][0-9]*$" >nul
It seems odd to me that both the original and your modified code simply pass through the username if it does not match your regex. Maybe that is your intent, maybe not.