RegEx in Batch File - regex

Hey I'm trying to create a function that parses a string passed via a browser protocol. It's a "callto://" protocol and it is in this format: "callto://5551234567/" with the persons phone number inside there. I need to extract the number and pass it to another program that dials the number. The syntax for that other program is like this: "CallClerk.exe dial=5551234567=".
I'm a beginner to batch however, and can't figure out exactly what to do. Here's my current code:
#echo off
set var=%1
set number=theirphone
FindStr /R "callto://(..........)/" %var% > %number%
start C:\Program Files (x86)\CallClerk\CallClerk.exe dial=%number%=
Exit /B
Thanks for the help!

#echo off
FOR /f "tokens=2 delims=/" %%i IN ('echo %~1') DO start "" "C:\Program Files (x86)\CallClerk\CallClerk.exe" dial=%%i=
Exit /B
should work for you (untested) - assuming your input parameter is callto://5551234567/
Note the use of quoting - the .exe needs to be quoted since it contains a space in the path. The extra pair of quotes in the window-name. If you like, you could replace that pair with "Calling %%i". This parameter is optional, but inserting it ensures that START doesn't get confused between window-title, executable-name and parameter-to-executable.

This works to extract numbers from a string.
It uses two for loops, the first one gathers all the non-numeric characters and they are used as delimiters in the second for loop to gather the numerics and dial the number.
Strings of variable lengths can be handled, as long as all numbers are used in the desired telephone number.
If you want to keep the + as a valid telephone character then include it in the first for command in the delims with the numbers.
#echo off
set "var=callto://5551234567/"
for /f "delims=0123456789" %%a in ("%var%") do set "delims=%%a"
for /f "delims=%delims%" %%a in ("%var%") do (
start "" "C:\Program Files (x86)\CallClerk\CallClerk.exe" dial=%%a=
)

You should be able to use a regex along the lines of (?<=callto:\/\/)[\d]+(?=\/) to grab the number itself. This uses a positive look ahead and look behind to make sure you are matching at least one number that is preceded by the callto:// and followed by a /.
If you left it as something like callto:\/\/[\d]+\/, then it is matching the entire string and will return back with the callto text included. If you are intending to pass just the numbers along to the next part of you code, extract them using the look ahead to guarantee the before and after conditions are met.
I did a quick test using the strings you used in your example. You can see the regex in action here.

Related

How can I use FINDSTR to place a serial number from a txt file in a variable?

I have a script that outputs a serials.txt file like this:
Serial Number
88PRDQ1
Serial Number
CZSQZV1
I would like to capture the service tag in a variable for future manipulation in the script.
My code seems to fail entirely and I can't find much in the way of what I am looking for with FINDSTR.
for /f "delims=" %%S in (serials.txt) do (findstr /r "^......[^ ]")
Since the output can be 6-7 long and any letter or number, I am struggling to get it to behave properly. It runs forever at the moment and I am unsure how to output the result to a variable and act on it if I somehow managed to get this line correct.
The [^ ] is meant to exclude spaces but accept anything else so as to not print the line that says "Serial Number" in the event of a 6 char serial.
What am I missing?
findstr /v /c:"Serial Number" serials.txt|findstr "."
First finds all line not containing the string "Serial Number", then filters any line containing at least one character to suppress empty lines.

Which regex method is best for validating user input? (for /f with delims vs. echo %var%|Findstr /ri)

I would like to validate a user's input and limit the input to alphanumeric characters only (underscores may be allowed as well), but i'm not sure which method is best for this.
I've seen various examples on SA and the first one that raises some questions for me is the following one:
:input
set "in="
set /p "in=Please enter your username: "
ECHO(%in%|FINDSTR /ri "^[0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ][0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ]*$" >nul || (
goto input
)
I see a second case that's identical to the first one (with as expection, the leading ^ and ending *$).
Why is the extra case and ^ *$ needed when the following also works?:
:input
set "in="
set /p "in=Please enter your username: "
ECHO(%in%|FINDSTR /ri "[0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ]" >nul || (
goto input
)
Finally, The FOR /F loop method i've noticed on here as well:
for /f "delims=1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ" %%a in ("%in%") do goto :input
Is there any (dis)advantage in using this over the beforementioned FINDSTR regex one?
For safely validating user input, both methods are reliable, but you must improve them:
findstr method
At first, let us focus on the search string like ^[...][...]*$ (where ... stands for a character class, meaning a set of characters): A character class [...] matches any one character from set ...; * means repetition, so matching zero or more occurrences, hence [...]* matches zero or more occurrences of characters from set ...; therefore, [...][...]* matches one or more occurrences of characters from set .... The leading ^ anchors the match to the beginning of the line, the trailing $ anchors it to the end; therefore, when both anchors are specified, the entire line must match the search string.
Concerning character classes [...]: According to the thread What are the undocumented features and limitations of the Windows FINDSTR command?, classes are buggy; for instance, the class [A-Z] matches small letters b to z, and [a-z] matches capital letters A to Y (this does of course not matter in case a case-insensitive search is done, so when /I is given); the class [0-9] may match ² or ³, depending on the current code page; [A-Z] and [a-z] may match special letters like Á or á, for example, also depending on current code page. Hence to safely match certain characters only, do not use ranges, but specify each character individually, like [0123456789], [ABCDEFGHIJKLMNOPQRSTUVWXYZ] or [abcdefghijklmnopqrstuvwxyz].
All this leads us to the following findstr command line:
findstr /R /I "^[0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ][0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ]*$"
Nevertheless, the whole approach with the piped echo might still fail, because special characters like ", &, ^, %, !, (, ), <, >, | could lead to syntax errors or other unintended behaviour. To avoid that, we need to establish delayed expansion, so the special characters become hidden from the command parser. However, since pipes (|) initialise new cmd instances for either side (which inherit the current environment), we need to ensure to do the actual variable expansion in the left child cmd instance rather than in the parent one, like this:
:INPUT
set "IN="
set /P IN="Please enter your username: "
cmd /V /C echo(^^!IN^^!| findstr /R /I "^[0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ][0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ]*$" > nul || goto :INPUT
The extra explicit cmd instance is needed to enable delayed expansion (/V), because the instances initiated by the pipe have delayed expansion disabled.
The doubled escaping of the exclamation marks ^^! is only needed in case delayed expansion is also enabled in the parent cmd instance; if not, single escaping ^! was sufficient, but doubled escaping does not harm.
for /F method
This approach makes life easier, because there is no pipe involved and so, you do not have to deal with multiple cmd instances, but there is still room for improvement. Again, special characters may cause trouble, so delayed expansion needs to be enabled.
The for /F loop ignores empty lines and such beginning with the default eol character, the semicolon ;. To disable the eol option, simply define one of the delimiter characters, so eol becomes hidden behind delims. Empty lines are not iterated, so the goto command in your approach would never execute in case of empty user input. Therefore, we must capture empty user input explicitly, using an if statement. Now all this leads to the following code:
setlocal EnableDelayedExpansion
:INPUT
set "IN="
set /P IN="Please enter your username: "
if not defined IN goto :INPUT
for /F "delims=0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ eol=0" %%Z in ("!IN!") do goto :INPUT
endlocal
This approach detects capital letters only; to include small letters as well, you have to add them to the delims option: delims=0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz.
Note that variable IN is no longer available beyond endlocal, but this should be the very last comand of your script anyway.
To detect whether or not a for /F loop iterated or not, there is an undocumented feature, which we can make use of: for /F returns a non-zero exit code if it does not iterate, hence conditional execution operators && or || can be used; so, when the user input is empty, the loop does not iterate, then ||; for this to work, the for /F loop must be enclosed within parentheses:
setlocal EnableDelayedExpansion
:INPUT
set "IN="
set /P IN="Please enter your username: "
if not defined IN goto :INPUT
(for /F "delims=0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ eol=0" %%Z in ("!IN!") do rem/) && goto :INPUT
endlocal
First, you have to reference environment variable in with using delayed expansion to avoid an exit of batch file execution because of a syntax error when the user enters a string with critical characters like ><|&". Always take into account that a variable specified with %variable% is expanded before execution of the command line which can easily break batch execution on user input variable strings.
Second, it is strongly recommended to immediately verify if the user has input anything at all after the prompt, i.e. use if not defined in goto input after the prompt command line.
Third, I think the FOR method is better because of being faster.
FINDSTR is not an internal command of cmd.exe like FOR. So when specifying FINDSTR in batch file without path and without file extension Windows command interpreter must first search for this executable and hopefully really finds %SystemRoot%\System32\findstr.exe via PATHEXT and PATH.
Next with an anti-virus process running in background the execution of findstr.exe triggers the scanning process of anti-virus process which results in a delay of execution.
The execution of an application like FINDSTR by Windows command interpreter takes always a bit longer as the execution of an internal command of cmd.exe even with no anti-virus scan process running. So the FOR loop approach is most likely (not verified by me) faster than the FINDSTR approach.
On using FINDSTR the regular expression characters ^ and *$ are needed because the regular expression search string [0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ] results in a positive match if the processed line contains anywhere at least 1 digit or letter. So it is not checked if the line (= string of variable) consists of only digits and letters. The shorter character class definitions [0-9A-Z] with depending on option /I or [0-9A-Za-z] can't be used in this case as explained by aschipfl in his comment below.
With ^ is specified that the searched string must be found at beginning of a line, with * that 0 or more digits or letters must be found, and with $ that the searched string must be found at end of line. Or in other words the entire line (user input) not being completely empty as checked before must completely consist of only digits and letters for a positive match.
For every internal or external command help on command can be get by running the command from within a command prompt window with /? as parameter. Try it out with opening a command prompt window and run findstr /? and for /? and set /?.

Using findstr to pass to a variable

I've got some files I'm running with a batch file that loops through everyone in a directory and dumps certain data into a sql table. I'm adding in a time stamp that I'm passing into a variable and trying to add to the sql table using sqlcmd the only problem is that to add in all relevant columns for that entry, I need to pass the names of the files that are being added to the sql table.
Okay here's the catch... the names being added to the sql table aren't the actual file names but database names that can be found in each of these xml files (close enough to xml). So I know where that is and every single one looks something like this abcdir (rest of the name) where the abcdir is a string that starts every single database.
So I thought I could use the findstr function to get the database name but I have very little experience with regex and I'd like to be able to parse out the tags and be left with just name=abcdir (rest of the name)
** * I didn't think any of my code would really be necessary since I'm just asking questions about a particular command but if thats not the case then let me know and I'll post it* **
EDIT: Okay so each file will have something like this if opened in notepad.
<Name>ABCDir Sample Name</Name>
or
<Name>ABCDir Sample Name2</Name>
and I'd like ABCDir Sample Name to be passed to a batch variable. So I thought to use findstr.
I have very little grasp of regex but I've tried using findstr >ABCDir[A-Za-z] \path\filename.ext
As I commented above, findstr (or find) will let you scrape lines containing <Name> from a text file, and for /f "delims=<>" will let you split those lines into substrings. With findstr /n, you're looking for "tokens=3 delims=<>" to get the string between <Name> and </Name>.
Try this:
#echo off
setlocal
set "file=temp.txt"
for /f "tokens=3 delims=<>" %%I in ('findstr /n /i "<Name>" "%file%"') do (
#echo %%I
)
I'm using /n with findstr to insert line numbers. The numbers aren't needed, but the switch ensures there's always a token before <Name>. Therefore, the string you want is always tokens=3 regardless of whether the line is indented or not. Otherwise, your string could be token 3 if indented, or token 2 if not. This is easier than trying to determine whether the tags are indented or not.

Writing .txt files from lines 1 to i

I am very close to my answer, but I cant seem to find it.
I am using the Findstr function in a batch file to narrow now an entire directory to just one file.
cd ...
findstr /s /m "Desktop" *class.asasm >results1.txt
findstr /m /f:results1.txt "Production" *class.asasm >results2.txt
findstr /n /f:results2.txt "Capabilities" *class.asasm >results3.txt
TASK 1: I need to figure out a way to make findstr search backwards for a fourth string from the line number the third line was found on
TASK 2: I need to write lines 1-the one we arrive at of the file in results2.txt
the insert a .txt file. Then write the rest of the original lines.
I am writing an application in VB.Net with Visual Studios and I am having a difficult time figuring out how to complete this process. I am currently having better luch with having the application run batch files that are written within the application.
The correct solution is to find a tool that does this properly. batch/CMD does not.
Here's a script that tells you the line numbers of the 3rd and 4th match. It's probably not exactly what you want, but it is a demonstration of how one can effectively work with line numbers.
#ECHO OFF
SETLOCAL ENABLEDELAYEDEXPANSION
SET FILE=TestFile.txt
SET _LINENO=1
SET _MATCHNO=0
SET _THIRDLINENUM=
SET _FOURTHLINENUM=
FOR /F %%l IN (%FILE%) DO (
ECHO %%l | FINDSTR "Target" %_TMP% >NUL
IF NOT ERRORLEVEL 1 (
SET /A _MATCHNO=!_MATCHNO!+1
IF !_MATCHNO!==3 SET _THIRDLINENUM=!_LINENO!
IF !_MATCHNO!==4 SET _FOURTHLINENUM=!_LINENO!
)
SET /A _LINENO=!_LINENO!+1
)
#ECHO %_THIRDLINENUM% : %_FOURTHLINENUM%
Here's what's in TestFile.txt
abcdefg
bcdefgh
Target 1
cdefghi
defghij
fghijkl
Target 2
ghijklm
hijklmn
ijklmno
jklmnop
klmnopq
lmnopqr
mnopqrs
Target 3
nopqrst
Target 4
opqrstu
pqrstuv
qrstuvw
rstuvwx
stuvwxy
tuvwxyz
If you insist on using batch/CMD (and I sometimes do when nothing else is available), and you need to get the text on line #n (otherwise, head and tail would do just fine), you could produce a similar loop but replace the code from FINDSTR down to the end of the IF statement with something that compares _LINENO with some other variable, ECHO'ing the line if it is between the two values. I don't know if IF supports logical operators, so you may have to nest the IF statements, like
IF !_LINENO! GEQ %START_LINE% IF !_LINENO! LEQ %END_LINE% #ECHO %%l
assuming you need this (from your first comment):
I still have not found a way to search starting at line xx rather than 1 or to search in reverse order
you can try this (from the command line):
for /r %i in ("file pattern") do #more "%~i" +starting_line |findstr "search string"
for /r = recursively (if you mean really reverse, please explain)
"file pattern" = files to find, eg. "*class.asasm"
starting_line = search starting line, eg. 7 (more +6)
"search string" = your search pattern, eg. "Desktop"
OR search "Desktop Production Capabilities"
AND search |findstr "Desktop"|findstr "Production"|findstr "Capabilities"

Piped Variable Into FINDSTR w/ Regular Expressions and Escaped Double Quotes

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.