How to Pass a Variable Between Batch and VBS? - batch-file

I have made a perfect square number finder that starts from 1 and the code is below. However, I need a way to pass variables between batch and vbscript (VBS has a better UI). I am stuck at this point, how do I send a variable between a batch and vbscript? If anything maybe have the vbscript print a number into a file then the batch script reads the file and the number in it then deletes it.
#echo off
title Perfect Squares without paste to file
cls
set /a num=3
set /a num1=1
echo 1
:1
set /a num2=%num1%+%num%
echo %num2%
echo %num2%
set /a num1=%num2%
set /a num=%num%+2
goto 1

You can run a VBScript from a batch file like this (the parameter //NoLogo prevents the interpreter from printing the version/copyright banner all the time):
cscript //NoLogo C:\your.vbs 23
Use the Arguments property for receiving the argument (23) in the VBScript:
value = WScript.Arguments(0)
and pass a value back to the batch script via the return value:
WScript.Quit 42
Example:
VBScript code:
value = WScript.Arguments(0)
WScript.Quit value + 19
Batch code:
#echo off
cscript //NoLogo C:\your.vbs %1
echo %errorlevel%
Output:
C:\>batch.cmd 23
42
C:\>batch.cmd 4
23
If you need to pass text back and forth, passing the response back to the batch script becomes more complicated. You'll need something like this:
for /f "tokens=*" %%r in ('cscript //NoLogo C:\your.vbs %1') do set "res=%%r"
Example:
VBScript code:
str = WScript.Arguments(0)
WScript.StdOut.WriteLine str & " too"
Batch code:
#echo off
setlocal
for /f "tokens=*" %%r in ('cscript //NoLogo C:\your.vbs %1') do set "res=%%r"
echo %res%
Output:
C:\>batch.cmd "foo"
foo too
C:\>batch.cmd "and some"
and some too
On a more general note: why do you use batch/VBScript in the first place? PowerShell is far more versatile than both of them combined, and is available for any halfway recent Windows version.

Related

Remove empty lines and spaces from .txt-File with Batch [duplicate]

What is the Windows batch equivalent of the Linux shell command echo -n which suppresses the newline at the end of the output?
The idea is to write on the same line inside a loop.
Using set and the /p parameter you can echo without newline:
C:\> echo Hello World
Hello World
C:\> echo|set /p="Hello World"
Hello World
C:\>
Source
Using: echo | set /p= or <NUL set /p= will both work to suppress the newline.
However, this can be very dangerous when writing more advanced scripts when checking the ERRORLEVEL becomes important as setting set /p= without specifying a variable name will set the ERRORLEVEL to 1.
A better approach would be to just use a dummy variable name like so:
echo | set /p dummyName=Hello World
This will produce exactly what you want without any sneaky stuff going on in the background as I had to find out the hard way, but this only works with the piped version; <NUL set /p dummyName=Hello will still raise the ERRORLEVEL to 1.
The simple SET /P method has limitations that vary slightly between Windows versions.
Leading quotes may be stripped
Leading white space may be stripped
Leading = causes a syntax error.
See http://www.dostips.com/forum/viewtopic.php?f=3&t=4209 for more information.
jeb posted a clever solution that solves most of the problems at Output text without linefeed, even with leading space or = I've refined the method so that it can safely print absolutely any valid batch string without the new line, on any version of Windows from XP onward. Note that the :writeInitialize method contains a string literal that may not post well to the site. A remark is included that describes what the character sequence should be.
The :write and :writeVar methods are optimized such that only strings containing troublesome leading characters are written using my modified version of jeb's COPY method. Non-troublesome strings are written using the simpler and faster SET /P method.
#echo off
setlocal disableDelayedExpansion
call :writeInitialize
call :write "=hello"
call :write " world!%$write.sub%OK!"
echo(
setlocal enableDelayedExpansion
set lf=^
set "str= hello!lf!world^!!!$write.sub!hello!lf!world"
echo(
echo str=!str!
echo(
call :write "str="
call :writeVar str
echo(
exit /b
:write Str
::
:: Write the literal string Str to stdout without a terminating
:: carriage return or line feed. Enclosing quotes are stripped.
::
:: This routine works by calling :writeVar
::
setlocal disableDelayedExpansion
set "str=%~1"
call :writeVar str
exit /b
:writeVar StrVar
::
:: Writes the value of variable StrVar to stdout without a terminating
:: carriage return or line feed.
::
:: The routine relies on variables defined by :writeInitialize. If the
:: variables are not yet defined, then it calls :writeInitialize to
:: temporarily define them. Performance can be improved by explicitly
:: calling :writeInitialize once before the first call to :writeVar
::
if not defined %~1 exit /b
setlocal enableDelayedExpansion
if not defined $write.sub call :writeInitialize
set $write.special=1
if "!%~1:~0,1!" equ "^!" set "$write.special="
for /f delims^=^ eol^= %%A in ("!%~1:~0,1!") do (
if "%%A" neq "=" if "!$write.problemChars:%%A=!" equ "!$write.problemChars!" set "$write.special="
)
if not defined $write.special (
<nul set /p "=!%~1!"
exit /b
)
>"%$write.temp%_1.txt" (echo !str!!$write.sub!)
copy "%$write.temp%_1.txt" /a "%$write.temp%_2.txt" /b >nul
type "%$write.temp%_2.txt"
del "%$write.temp%_1.txt" "%$write.temp%_2.txt"
set "str2=!str:*%$write.sub%=%$write.sub%!"
if "!str2!" neq "!str!" <nul set /p "=!str2!"
exit /b
:writeInitialize
::
:: Defines 3 variables needed by the :write and :writeVar routines
::
:: $write.temp - specifies a base path for temporary files
::
:: $write.sub - contains the SUB character, also known as <CTRL-Z> or 0x1A
::
:: $write.problemChars - list of characters that cause problems for SET /P
:: <carriageReturn> <formFeed> <space> <tab> <0xFF> <equal> <quote>
:: Note that <lineFeed> and <equal> also causes problems, but are handled elsewhere
::
set "$write.temp=%temp%\writeTemp%random%"
copy nul "%$write.temp%.txt" /a >nul
for /f "usebackq" %%A in ("%$write.temp%.txt") do set "$write.sub=%%A"
del "%$write.temp%.txt"
for /f %%A in ('copy /z "%~f0" nul') do for /f %%B in ('cls') do (
set "$write.problemChars=%%A%%B  ""
REM the characters after %%B above should be <space> <tab> <0xFF>
)
exit /b
As an addendum to #xmechanix's answer, I noticed through writing the contents to a file:
echo | set /p dummyName=Hello World > somefile.txt
That this will add an extra space at the end of the printed string, which can be inconvenient, specially since we're trying to avoid adding a new line (another whitespace character) to the end of the string.
Fortunately, quoting the string to be printed, i.e. using:
echo | set /p dummyName="Hello World" > somefile.txt
Will print the string without any newline or space character at the end.
A solution for the stripped white space in SET /P:
the trick is that backspace char which you can summon in the text editor EDIT for DOS. To create it in EDIT press ctrlP+ctrlH.
I would paste it here but this webpage can't display it. It's visible on Notepad though (it's werid, like a small black rectangle with a white circle in the center)
So you write this:
<nul set /p=.9 Hello everyone
The dot can be any char, it's only there to tell SET /P that the text starts there, before the spaces, and not at the "Hello".
The "9" is a representation of the backspace char that I can't display here. You have to put it instead of the 9, and it will delete the "." , after which you'll get this:
Hello Everyone
instead of:
Hello Everyone
I hope it helps
Here is another method, it uses Powershell Write-Host which has a -NoNewLine parameter, combine that with start /b and it offers the same functionality from batch.
NoNewLines.cmd
#ECHO OFF
start /b /wait powershell.exe -command "Write-Host -NoNewLine 'Result 1 - ';Write-Host -NoNewLine 'Result 2 - ';Write-Host -NoNewLine 'Result 3 - '"
PAUSE
Output
Result 1 - Result 2 - Result 3 - Press any key to continue . . .
This one below is slightly different, doesn't work exactly like the OP wants, but is interesting because each result overwrites the previous result emulating a counter.
#ECHO OFF
start /b /wait powershell.exe -command "Write-Host -NoNewLine 'Result 1 - '"
start /b /wait powershell.exe -command "Write-Host -NoNewLine 'Result 2 - '"
start /b /wait powershell.exe -command "Write-Host -NoNewLine 'Result 3 - '"
start /b /wait powershell.exe -command "Write-Host -NoNewLine 'Result 4 - '"
start /b /wait powershell.exe -command "Write-Host -NoNewLine 'Result 5 - '"
start /b /wait powershell.exe -command "Write-Host -NoNewLine 'Result 6 - '"
start /b /wait powershell.exe -command "Write-Host -NoNewLine 'Result 7 - '"
start /b /wait powershell.exe -command "Write-Host -NoNewLine 'Result 8 - '"
start /b /wait powershell.exe -command "Write-Host -NoNewLine 'Result 9 - '"
PAUSE
You can remove the newline using "tr" from gnuwin32 (coreutils package)
#echo off
set L=First line
echo %L% | tr -d "\r\n"
echo Second line
pause
By the way, if you are doing lots of scripting, gnuwin32 is a goldmine.
I made a function out of #arnep 's idea:
echo|set /p="Hello World"
here it is:
:SL (sameline)
echo|set /p=%1
exit /b
Use it with call :SL "Hello There"
I know this is nothing special but it took me so long to think of it I figured I'd post it here.
DIY cw.exe (console write) utility
If you don't find it out-of-the-box, off-the-shelf, you can DIY. With this cw utility you can use every kind of characters. At least, I'd like to think so. Please stress-test it and let me know.
Tools
All you need is .NET installed, which is very common nowadays.
Materials
Some characters typed/copy-pasted.
Steps
Create .bat file with the following content.
/* >nul 2>&1
#echo off
setlocal
set exe=cw
for /f "tokens=* delims=" %%v in ('dir /b /s /a:-d /o:-n "%SystemRoot%\Microsoft.NET\Framework\*csc.exe"') do set "csc=%%v"
"%csc%" -nologo -out:"%exe%.exe" "%~f0"
endlocal
exit /b %errorlevel%
*/
using System;
namespace cw {
class Program {
static void Main() {
var exe = Environment.GetCommandLineArgs()[0];
var rawCmd = Environment.CommandLine;
var line = rawCmd.Remove(rawCmd.IndexOf(exe),exe.Length).TrimStart('"');
line = line.Length < 2 ? "\r" : line.Substring(2) ;
Console.Write(line);
}
}
}
Run it.
Now you have a nice 4KB utility so you can delete the .bat.
Alternatively, you can insert this code as a subroutine in any batch, send the resulting .exe to %temp%, use it in your batch and delete it when you're done.
How to use
If you want write something without new line:
cw Whatever you want, even with "", but remember to escape ^|, ^^, ^&, etc. unless double-quoted, like in "| ^ &".
If you want a carriage return (going to the beginning of the line), run just
cw
So try this from command line:
for /l %a in (1,1,1000) do #(cw ^|&cw&cw /&cw&cw -&cw&cw \&cw)
From here
<nul set /p =Testing testing
and also to echo beginning with spaces use
echo.Message goes here
Maybe this is what your looking for, it's a old school script... :P
set nl=^& echo.
echo %nl%The%nl%new%nl%line%nl%is%nl%not%nl%apparent%nl%throughout%nl%text%nl%
echo only in prompt.
pause
or maybe your trying to replace a current line instead of writing to a new line?
you can experiment with this by removing the "%bs%" after the "." sign and also by spacing out the other "%bs%" after the "Example message".
for /f %%a in ('"prompt $H&for %%b in (1) do rem"') do set "bs=%%a"
<nul set /p=.%bs% Example message %bs%
pause
I find this really interesting because it uses a variable for a purpose other than what it is intended to do. as you can see the "%bs%" represents a backspace. The second "%bs%" uses the backspace to add spaces after the "Example message" to separate the "Pause command's output" without actually adding a visible character after the "Example message". However, this is also possible with a regular percentage sign.
Sample 1: This works and produces Exit code = 0. That is Good.
Note the "." , directly after echo.
C:\Users\phife.dog\gitrepos\1\repo_abc\scripts #
#echo.| set /p JUNK_VAR=This is a message displayed like Linux echo -n would display it ... & echo %ERRORLEVEL%
This is a message displayed like Linux echo -n would display it ... 0
Sample 2: This works but produces Exit code = 1. That is Bad.
Please note the lack of ".", after echo. That appears to be the difference.
C:\Users\phife.dog\gitrepos\1\repo_abc\scripts #
#echo | set /p JUNK_VAR=This is a message displayed like Linux echo -n would display it ... & echo %ERRORLEVEL%
This is a message displayed like Linux echo -n would display it ... 1
Inspired by the answers to this question, I made a simple counter batch script that keeps printing the progress value (0-100%) on the same line (overwritting the previous one). Maybe this will also be valuable to others looking for a similar solution.
Remark: The * are non-printable characters, these should be entered using [Alt + Numpad 0 + Numpad 8] key combination, which is the backspace character.
#ECHO OFF
FOR /L %%A in (0, 10, 100) DO (
ECHO|SET /P="****%%A%%"
CALL:Wait 1
)
GOTO:EOF
:Wait
SET /A "delay=%~1+1"
CALL PING 127.0.0.1 -n %delay% > NUL
GOTO:EOF
You can suppress the new line by using the set /p command. The set /p command does not recognize a space, for that you can use a dot and a backspace character to make it recognize it. You can also use a variable as a memory and store what you want to print in it, so that you can print the variable instead of the sentence. For example:
#echo off
setlocal enabledelayedexpansion
for /f %%a in ('"prompt $H & for %%b in (1) do rem"') do (set "bs=%%a")
cls
set "var=Hello World! :)"
set "x=0"
:loop
set "display=!var:~%x%,1!"
<nul set /p "print=.%bs%%display%"
ping -n 1 localhost >nul
set /a "x=%x% + 1"
if "!var:~%x%,1!" == "" goto end
goto loop
:end
echo.
pause
exit
In this way you can print anything without a new line. I have made the program to print the characters one by one, but you can use words too instead of characters by changing the loop.
In the above example I used "enabledelayedexpansion" so the set /p command does not recognize "!" character and prints a dot instead of that. I hope that you don't have the use of the exclamation mark "!" ;)
Use EchoX.EXE from the terrific "Shell Scripting Toolkit" by Bill Stewart
How to suppress the linefeed in a Windows Cmd script:
#Echo Off
Rem Print three Echos in one line of output
EchoX -n "Part 1 - "
EchoX -n "Part 2 - "
EchoX "Part 3"
Rem
gives:
Part 1 - Part 2 - Part 3
{empty line}
d:\Prompt>
The help for this usage is:
Usage: echox [-n] message
-n Do not skip to the next line.
message The text to be displayed.
The utility is smaller than 48K, and should live in your Path. More things it can do:- print text without moving to the next line- print text justified to the left, center, or right, within a certain width- print text with Tabs, Linefeeds, and Returns- print text in foreground and background colors
The Toolkit includes twelve more great scripting tricks.
The download page also hosts three other useful tool packages.
I found this simple one-line batch file called "EchoPart.bat" to be quite useful.
#echo | set /p=%*
I could then write something like the line below even on an interactive CMD line, or as part of a shortcut. It opens up a few new possibilities.
echopart "Hello, " & echopart "and then " & echo Goodbye
And if you're using it in batch files, the texts can be got from parameter variables instead of immutable strings. For instance:
#echopart Hello %* & #echo , how are you?
So that executing this line in "SayHello.bat" allows:
or even...
Have a play, and have fun!
I believe there's no such option. Alternatively you can try this
set text=Hello
set text=%text% world
echo %text%
Echo with preceding space and without newline
As stated by Pedro earlier, echo without new line and with preceding space works (provided "9" is a true [BackSpace]).
<nul set /p=.9 Hello everyone
I had some issues getting it to work in Windows 10 with the new console but managed the following way.
In CMD type:
echo .◘>bs.txt
I got "◘" by pressing [Alt] + [8]
(the actual symbol may vary depending upon codepage).
Then it's easy to copy the result from "bs.txt" using Notepad.exe to where it's needed.
#echo off
<nul set /p "_s=.◘ Hello everyone"
echo: here
With jscript:
#if (#X)==(#Y) #end /*
#cscript //E:JScript //nologo "%~nx0" %*
#exit /b %errorlevel%
*/if(WScript.Arguments.Count()>0) WScript.StdOut.Write(WScript.Arguments.Item(0));
if it is called write.bat you can test it like:
call write.bat string & echo _Another_String_
If you want to use powershell but with cmd defined variables you can use:
set str=_My_StrinG_
powershell "Write-Host -NoNewline ""%str%"""" & echo #Another#STRING#
Late answer here, but for anyone who needs to write special characters to a single line who find dbenham's answer to be about 80 lines too long and whose scripts may break (perhaps due to user-input) under the limitations of simply using set /p, it's probably easiest to just to pair your .bat or .cmd with a compiled C++ or C-language executable and then just cout or printf the characters. This will also allow you to easily write multiple times to one line if you're showing a sort of progress bar or something using characters, as OP apparently was.

Batch: Pipe command output to variable and compare

I try to pipe my command output to variables and then compare if those variables have the same value. However it always return both variables have same value(even though it is not). Below is my code:
#echo off
goto main
:main
setlocal
echo 111 | (set /p readvalue= & set readvalue)
echo 123 | (set /p readvaluebash= & set readvaluebash)
if "%readvalue%"=="%readvaluebash%" goto valid
if NOT "%readvalue%"=="%readvaluebash%" goto invalid
:valid
echo yes
pause
goto finish
:invalid
echo no
pause
goto finish
:finish
endlocal
I always get the yes result. Anyone know my mistake here? Thanks in advance!
When you run (same for the other line)
echo 111 | (set /p readvalue= & set readvalue)
you see that the value shown in console as the variable value is 111, so the set /p was able to retrieve the piped data.
The problem is that the pipe operator starts two separate cmd instances: one running the left part (echo) of the pipe and another running the right part (set /p).
As each process has its own environment space and as the set /p is executed in a separate cmd instance, any change to any variable in this new cmd instace will not change the environment of the cmd instance running the batch file.
To store the output of a command in a variable, instead of a pipe you can use a for /f command
for /f "delims=" %%a in ('echo 111') do set "readValue=%%a"

Loop through ASCII codes in batch file

How do I loop through ASCII values (here, alphabets) and work on them?
I want to echo A to Z without having to type every character manually like
for %%J in (A B C D E F G H I J K L M N O P Q R S T U V W X Y Z) do echo %%J
So I was wondering if I can cycle through ASCII codes. Something like for %%J in (ASCII of A ie.65 to Z) do echo %%J
Any help would be appreciated.
Surprisingly, there is a solution that makes use of an undocumented built-in environment variable named =ExitCodeAscii, which holds the ASCII character of the current exit code1 (ErrorLevel):
#echo off
for /L %%A in (65,1,90) do (
cmd /C exit %%A
call echo %%^=ExitCodeAscii%%
)
The for /L loop walks through the (decimal) character codes of A to Z. cmd /C exit %%A sets the return code (ErrorLevel) to the currently iterated code, which is echo-ed as a character afterwards. call, together with the double-%-signs introduce a second parsing phase for the command line in order to get the current value of =ExitCodeAscii rather than the one present before the entire for /L loop is executed (this would happen with a simple command line like echo %=ExitCodeAscii%). Alternatively, delayed expansion could be used also.
The basic idea is credited to rojo and applied in this post: How do i get a random letter output in batch.
1) The exit code (or return code) is not necessarily the same thing as the ErrorLevel value. However, the command line cmd /C exit 1 sets both values to 1. To ensure that the exit code equals ErrorLevel, use something like cmd /C exit %ErrorLevel%.
Loop through ASCII codes in batch file
Any batch solution to do what you want would be much more complex than what you already have.
Consider using PowerShell instead:
for($i=65;$i -le 90; $i++){[char]$i}
Example output:
PS F:\test> for($i=65;$i -le 90; $i++){[char]$i}
A
B
C
D
E
F
G
H
I
J
K
L
M
N
O
P
Q
R
S
T
U
V
W
X
Y
Z
This answer is a compilation and comparison of the methods given before.
In the question the OP requested to echo A to Z letters in a batch file. If the purpose of the solution is not just show letters, but process the letters in any other way, then the =ExitCodeAscii variable method is the only one that do that with Batch code, although the modification required in the other two methods to do the same is simple.
The code below include the three methods and compare they in the simplest possible way: via the time required by each one to complete.
#if (#CodeSection == #Batch) #then
#echo off
setlocal
set "start=%time%"
for /L %%a in (65,1,90) do (
cmd /C exit %%a
call echo Batch: %%^=ExitCodeAscii%%
)
echo First method: using =ExitCodeAscii variable
echo Start: %start%
echo End: %time%
echo/
set "start=%time%"
for /F %%a in ('powershell "65..90 | %%{ [char]$_ }"') do echo PS: %%a
echo Second method: using PowerShell
echo Start: %start%
echo End: %time%
echo/
set "start=%time%"
for /F %%a in ('cscript //nologo //E:JScript "%~F0"') do echo JScript: %%a
echo Third method: using JScript
echo Start: %start%
echo End: %time%
goto :EOF
#end
for (var i=65; i<=90; i++) WSH.Echo(String.fromCharCode(i));
Several executions of this code show consistent results: the PowerShell method is the slowest one, the =ExitCodeAscii method run 280% faster than PowerShell, and JScript method run 240% faster than =ExitCodeAscii. These differences would diminish as the whole program grow and perform more things than just show the letters, but in standard/small Batch files, this relation will always be the same: PowerShell is the slowest method and JScript the fastest one. The VBScript method is similar to JScript.
In Vbscript, you can do something like this :
For i = 65 To 90
Car = Car & Chr(i) & vbTab
Next
wscript.echo Car
And you can generate it with a batch file like this :
#echo off
Call :vbs
Pause
exit /b
:vbs
(
echo For i = 65 To 90
echo Car = Car ^& Chr(i^) ^& vbTab
echo Next
echo wscript.echo Car
)>"%tmp%\%~n0.vbs"
Cscript /NoLogo "%tmp%\%~n0.vbs"
The simplest solution is to include this PowerShell one-liner in your bat script:
powershell "[char[]](65..90)"
It's not necessarily the fastest, though.
Here's a VBScript solution similar to Hackoo's, but in a hybrid format not relying on writing an external .vbs file. Save it with a .bat extension. The cscript line is what triggers execution of the VBScript hybrid code.
<!-- : batch portion
#echo off & setlocal
cscript /nologo "%~f0?.wsf"
goto :EOF
: VBScript -->
<job>
<script language="VBScript">
For i = 65 to 90
WSH.Echo(Chr(i))
Next
</script>
</job>
Or if you're more comfortable with JavaScript syntax, here's a Batch + JScript hybrid.
#if (#CodeSection == #Batch) #then
#echo off & setlocal
cscript /nologo /e:JScript "%~f0"
goto :EOF
#end // end Batch / begin JScript
for (var i=65; i<=90; i++) WSH.Echo(String.fromCharCode(i));

Second instance of batch loop does not run

I am trying to create a DOS batch script on my Windows 7 machine. I want to execute file 123456.bat, which contains the following command and parameters:
call **startSelenium.bat** 55 someSuiteName suite-someSuite.html development 1 1 10 10
That script calls into the startSelenium.bat script below:
Setlocal EnableDelayedExpansion
SET TimeStamp=
FOR /f "tokens=1-4 delims=/ " %%i in ("%date%") do set datestr=%%l%%j%%k
FOR /f "tokens=1-4 delims=.: " %%i in ("%time%") do set timestr=%%i%%j%%k%%l
SET TimeStamp=%datestr%%timestr%
set a=2
set b=0
set /a c=%a%+%b%
FOR /l %%t IN (1, 1, %c%) do (
call :SEARCHPORT %%t
echo %startPort%
Start java -jar C:\Selenium\2\selenium-server-standalone-2.47.1.jar -port %startPort% -singleWindow -userExtensions C:\selenium\2\user-extensions.js -firefoxProfileTemplate "c:\selenium\2\ffprofiles\2rc" -htmlSuite "*chrome" "https://www.google.com" "Z:\selenium\2\environment\%4\suites\%3" "u:\results\%4\result-%1-%computername%-1234-%TimeStamp%.htm"
timeout /t 10
)
GOTO :EOF
:SEARCHPORT
netstat -o -n -a | find "LISTENING" | find ":%startPort% " > NUL
if "%ERRORLEVEL%" equ "0" (
set /a startPort +=1
GOTO :SEARCHPORT
) ELSE (
set freePort=%startPort%
echo %startPort%
GOTO :EOF
When I run the script, the first instance of the java applications runs with a free Windows port that the SEARCHPORT subroutine found; however, the second instance pops up and quits immediately. I suspect the code is using the same variable from the first time it went through the FOR loop instead of getting a new unused port number.
What am I doing wrong? I copied various parts of this code from other sources. I am obviously a nube, so plain English would be helpful. :)
You have got an delayed expansion issue.
You already enabled delayed expansion, but you don't use it. You have to replace %startPort% with !startPort! inside your for /l loop.

Calling function from included batch file with a parameter

In my main batch file I include another batch file and want to call a function defined in there, code looks like following:
#echo off
call define_wait.bat
if "%1"=="WAIT" (
call :WAIT_AND_PRINT 5
echo.
)
REM rest...
My define_wait.bat looks like following:
:WAIT_AND_PRINT
set /a time=%1
for /l %%x in (1, 1, %time%) do (
ping -n 1 -w 1000 1.0.0.0 > null
echo|set /p=.
)
goto :EOF
:WAIT
set /a time="%1 * 1000"
ping -n 1 -w %time% 1.0.0.0 > null
goto :EOF
The problem is that if I define the wait function in another batch file it does not work, calling call :WAIT_AND_PRINT 5 does not hand on the parameter correctly (Error: missing operand)... If I copy my code from my define_wait.bat int my main batch file, everything works fine...
How would I make that correctly?
Working function bat that forwards it's parameters to it's subfunction:
#echo off
call %*
goto :EOF
:WAIT_AND_PRINT
set /a time=%1
for /l %%x in (1, 1, %time%) do (
ping -n 1 -w 1000 1.0.0.0 > null
echo|set /p=.
)
goto :EOF
:WAIT
set /a time="%1 * 1000"
ping -n 1 -w %time% 1.0.0.0 > null
goto :EOF
In the main bat I now don't include the batch file anymore but call it directly like following:
call define_wait.bat :WAIT_AND_PRINT 5
I wasn't aware of this until jeb commented it, but here's a quick demonstration of the call bug he mentioned, using some utility functions I had lying around.
functions.bat:
:length <"string">
rem // sets errorlevel to the string length (not including quotation marks)
setlocal disabledelayedexpansion
if "%~1"=="" (endlocal & exit /b 0) else set ret=1
set "tmpstr=%~1"
for %%I in (4096 2048 1024 512 256 128 64 32 16 8 4 2 1) do (
setlocal enabledelayedexpansion
if not "!tmpstr:~%%I,1!"=="" (
for %%x in ("!tmpstr:~%%I!") do endlocal & (
set /a ret += %%I
set "tmpstr=%%~x"
)
) else endlocal
)
endlocal & exit /b %ret%
:password <return_var>
rem // prompts user for password, masks input, and sets return_var to entered value
setlocal disabledelayedexpansion
<NUL set /P "=Password? "
set "psCommand=powershell -noprofile "$p=read-host -AsSecureString;^
$m=[Runtime.InteropServices.Marshal];$m::PtrToStringAuto($m::SecureStringToBSTR($p))""
for /f "usebackq delims=" %%p in (`%psCommand%`) do endlocal & set "%~1=%%p"
goto :EOF
main.bat:
#echo off & setlocal
rem // demo return value
call :password pass
setlocal enabledelayedexpansion
echo You entered !pass!
rem // demo bubbling up of %ERRORLEVEL%
call :length "!pass!"
echo Password length is %ERRORLEVEL%
endlocal
goto :EOF
rem // ====== FUNCTION DECLARATIONS =======
:length <"string">
:password <return_var>
functions.bat %*
Output:
Password? *********
You entered something
Password length is 9
This web page offers an explanation:
If you execute a second batch file without using CALL you may run into some buggy behaviour: if both batch files contain a label with the same name and you have previously used CALL to jump to that label in the first script, you will find execution of the second script starts at the same label. Even if the second label does not exist this will still raise an error "cannot find the batch label". This bug can be avoided by always using CALL.
If you've ever done any coding in C++, it helps to think of the labels in main.bat as function declarations in a .h file, while the labels in functions.bat would correspond to function definitions in a .cpp file. Or in .NET, the main.bat labels would be like DllImport("functions.bat") so to speak.
Although there are several ways to call a function that reside in a separate library file, all methods require to change the way to call the library functions in the calling program, and/or insert additional code at beginning of the library file in order to identify the called function.
There is an interesting trick that allows to avoid all these details, so both the main and the library files contain the original code, and just 2 lines needs to be added to the main file. The method consist in switch the context of the running main Batch file to the library file; after that, all functions in the library file are available to the running code. The way to do that is renaming the library file with the same name of the main file. After that, when a call :function command is executed, the :function label will be search in the library file! Of course, the files must be renamed back to the original names before the program ends. Ah! I almost forget the key point of this method: both the initial and final renames must be executed in a code block in the main file. A simple example:
main.bat
#echo off
echo Calling :test and :hello functions in the library.bat file:
rem Switch the context to the library file
(ren "%~NX0" temp.bat & ren library.bat "%~NX0"
call :test
echo Back from library.bat :test function
call :hello
echo Back from library.bat :hello function
rem Switch the context back to the main file
ren "%~NX0" library.bat & ren temp.bat "%~NX0")
echo Continue in main file
library.bat
:test
echo I am :test function in library.bat file
exit /B
:hello
echo I am :hello function in library.bat file
exit /B
A drawback of this method is that if a run-time error happens when the files are renamed, the files remains renamed, but this may be fixed in a very simple way. For example, a check.bat file may check if the library.bat file exists, and do the rename back if it was not found.

Resources