46

People have been embedding and executing VBScript within batch files for a long time. But all the published solutions that I have seen (at the time this question was originally posed) involve writing a temporary VBS file. For example: Embed VBScript inside Windows batch file.

Is it possible to execute embedded VBScript within batch without writing a temporary file?

Community
  • 1
  • 1
dbenham
  • 119,153
  • 25
  • 226
  • 353

6 Answers6

64

Note - jump to the UPDATE 2014-04-27 section at the bottom of this answer for the best solution.

I used to think the answer was no. But then DosTips user Liviu discovered that the <SUB> character (Ctrl-Z, 0x1A, decimal 26) has bizare effects when embedded within a batch file. If functions somewhat like a line terminator such that it is possible for batch commands that follow a REM (or a :: remark hack) to execute if they are preceded by Ctrl-Z. http://www.dostips.com/forum/viewtopic.php?p=13160#p13160

This has been confirmed on XP Home Edition sp3, Vista Home Premium sp2 64 bit, and Vista Enterprise sp2 32 bit. I'm assuming it works on other Windows versions.

Note - the code below is supposed to have embedded Ctrl-Z characters. I swear I used to see them on this site when viewed with IE8. But they seem to have been lost from this post somehow and I cannot figure out how to post them anymore. I've replaced the characters with the string <SUB>

::<sub>echo This will execute in batch, but it will fail as vbs.
rem<SUB>echo This will execute in batch, and it is also a valid vbs comment
::'<SUB>echo This will execute in batch and it is also a valid vbs comment

That is the key to a successful batch/vbs hybrid. As long as each batch command is preceded by rem<SUB> or ::'<SUB>, then the vbs engine won't see it, but the batch command will run. Just make sure you terminate the batch portion with an EXIT or EXIT /B. Then the remainder of the script can be normal looking vbs.

You can even have a batch label if needed. :'Label is both a valid vbs comment and a valid batch label.

Here is a trivial hybrid script. (again with <SUB> in place of embedded Ctrl-Z char)

::'<SUB>@cscript //nologo //e:vbscript "%~f0" & exit /b
WScript.Echo "Example of a hybrid VBS / batch file"

Update 2012-04-15

jeb found a solution that avoids the awkward CTRL-Z, but it prints out ECHO OFF at the start and also sets some extraneous variables.

I have found a solution without CTRL-Z that eliminates the extraneous variables and is simpler to comprehend.

Normally the special characters &, |, <, > etc. don't work after a REM statement in batch. But the special characters do work after REM.. I found this nugget of information at http://www.dostips.com/forum/viewtopic.php?p=3500#p3500. A test shows that REM. is still a valid VBS comment. EDIT - based on jeb's comment, it is safer to use REM^ (there is a space after the caret).

So here is a trivial VBS/batch hybrid using REM^ &. The only drawback is it prints REM & at the beginning, whereas jeb's solution prints ECHO OFF.

rem^ &@cscript //nologo //e:vbscript "%~f0" & exit /b
WScript.Echo "Example of a hybrid VBS / batch file"

Here is another trivial example that demonstrates multiple batch commands, including a CALL to a labeled sub-routine.

::' VBS/Batch Hybrid

::' --- Batch portion ---------
rem^ &@echo off
rem^ &call :'sub
rem^ &exit /b

:'sub
rem^ &echo begin batch
rem^ &cscript //nologo //e:vbscript "%~f0"
rem^ &echo end batch
rem^ &exit /b

'----- VBS portion ------------
wscript.echo "begin VBS"
wscript.echo "end VBS"
'wscript.quit(0)

I still like the CTRL-Z solution because it eliminates all extraneous output.

UPDATE 2012-12-17

Tom Lavedas posted a method to conveniently run dynamic VBS from a batch script over at Google Groups: No file VBS hybrid scripting. The method uses mshta.exe (Microsoft HTML Application Host).

His original batch solution relied on an external small VBS.BAT script to execute the VBS within a FOR /F. I modified the syntax slightly to make it convenient to embed directly within any given batch script.

It is quite slow, but very convenient. It is restricted to executing a single line of VBS.

The VBS is written normally, except all quotes must be doubled: A quote enclosing a string must be written as "", and quotes internal to a string must be written as """". Normally the mini script is executed within the IN() clause of a FOR /F. It can be executed directly, but only if stdout has been redirected or piped.

It should work on any Windows OS from XP onward as long as IE is installed.

@echo off
setlocal
:: Define simple batch "macros" to implement VBS within batch
set "vbsBegin=mshta vbscript:Execute("createobject(""scripting.filesystemobject"")"
set "vbsBegin=%vbsBegin%.GetStandardStream(1).write("
set ^"vbsEnd=):close"^)"

:: Get yesterday's date
for /f %%Y in ('%vbsBegin% date-1 %vbsEnd%') do set Yesterday=%%Y
set Yesterday
pause
echo(

:: Get pi
for /f %%P in ('%vbsBegin% 4*atn(1) %vbsEnd%') do set PI=%%P
set PI
pause
echo(

set "var=name=value"
echo Before - %var%
:: Replace =
for /f "delims=" %%S in (
  '%vbsBegin% replace(""%var%"",""="","": "") %vbsEnd%'
) do set "var=%%S"
echo After  - %var%
pause
echo(

echo Extended ASCII:
for /l %%N in (0,1,255) do (

  %=  Get extended ASCII char, except can't work for 0x00, 0x0A.  =%
  %=  Quotes are only needed for 0x0D                             =%
  %=    Enclosing string quote must be coded as ""                =%
  %=    Internal string quote must be coded as """"               =%
  for /f delims^=^ eol^= %%C in (
    '%vbsBegin% """"""""+chr(%%N)+"""""""" %vbsEnd%'
  ) do set "char.%%N=%%~C"

  %=  Display result  =%
  if defined char.%%N (
    setlocal enableDelayedExpansion
    echo(   %%N: [ !char.%%N! ]
    endlocal
  ) else echo(   %%N: Doesn't work :(
)
pause
echo(

:: Executing the mini VBS script directly like the commented code below 
:: will not work because mshta fails unless stdout has been redirected
:: or piped.
::
::    %vbsBegin% ""Hello world"" %vbsEnd%
::

:: But this works because output has been piped
%vbsBegin% ""Hello world"" %vbsEnd% | findstr "^"
pause

UPDATE 2014-04-27

Over at DosTips there is a great compendium of js/vbs/html/hta hybrids and chimeras in cmd/bat. Lots of good stuff from various people.

Within that thread, DosTips user Liviu discovered a beautiful VBS/batch hybrid solution that uses WSF.

<!-- : Begin batch script
@echo off
cscript //nologo "%~f0?.wsf"
exit /b

----- Begin wsf script --->
<job><script language="VBScript">
  WScript.Echo "VBScript output called by batch"
</script></job>

I think this solution is fantastic. The batch and WSF sections are clearly separated by nice headers. The batch code is absolutely normal, without any odd syntax. The only restriction is the batch code cannot contain -->.

Similarly, the VBS code within WSF is absolutely normal. The only restriction is the VBS code cannot contain </script>.

The only risk is the undocumented use of "%~f0?.wsf" as the script to load. Somehow the parser properly finds and loads the running .BAT script "%~f0", and the ?.wsf suffix mysteriously instructs CSCRIPT to interpret the script as WSF. Hopefully MicroSoft will never disable that "feature".

Since the solution uses WSF, the batch script can contain any number of independent VBS, JScript, or other jobs that can be selectively called. Each job can even utilize multiple languages.

<!-- : Begin batch script
@echo off
echo batch output
cscript //nologo "%~f0?.wsf" //job:JS
cscript //nologo "%~f0?.wsf" //job:VBS
exit /b

----- Begin wsf script --->
<package>
  <job id="JS">
    <script language="VBScript">
      sub vbsEcho()
        WScript.Echo "VBScript output called by JScript called by batch"
      end sub
    </script>
    <script language="JScript">
      WScript.Echo("JScript output called by batch");
      vbsEcho();
    </script>
  </job>
  <job id="VBS">
    <script language="JScript">
      function jsEcho() {
        WScript.Echo("JScript output called by VBScript called by batch");
      }
    </script>
    <script language="VBScript">
      WScript.Echo "VBScript output called by batch"
      call jsEcho
    </script>
  </job>
</package>
dbenham
  • 119,153
  • 25
  • 226
  • 353
  • 3
    Your second solution with `REM.` is also nice, but have a drawback. If a file exists with the name `REM` (without extension), `REM.` results into a `file not found` error. But you could use `REM^:` instead, that seems to work always. (also `REM^,` `REM^;`, `REM^ `) – jeb Apr 15 '12 at 22:44
  • @jeb - It seems to me that `REM:` without the caret can never fail, and is the simplest of all. Or is there an obscure situation where it does fail? – dbenham Nov 01 '13 at 22:12
  • Yes `rem:\..\..\windows\system32\calc.exe` this fails also with `rem^:`, but obviously this case isn't important here – jeb Nov 02 '13 at 08:36
  • Why does CMD.EXE not say that the first line is invalid? – lit Dec 28 '14 at 17:05
  • 3
    @Paul - Believe it or not, that line is a valid batch label. CMD.EXE is chock full of idiosyncrasies like that. The characters before the colon are among a handful of characters that can appear before a batch label - something that jeb discovered and posted somewhere on DosTips. If you are willing to go down the rabbit hole, then you can read about a host of crazy label rules at http://www.dostips.com/forum/viewtopic.php?f=3&t=3803 – dbenham Dec 28 '14 at 18:28
  • this seems to work with batch file, but can I achieve this directly from the command line ? without file at all. – tinyfiledialogs Jan 25 '15 at 09:36
  • @chafporte - The mshta.exe solution under ***UPDATE 2012-12-17*** can be adapted for use on the command line. Ask a new question if you want details, as that does not fit within this question. But there is no way to get CSCRIPT to accept source code via the command line or an input stream. Look into PowerShell if you want sophisticated command line scripting – dbenham Jan 26 '15 at 04:02
  • For **UPDATE 2014-04-27**, you should change the line to read `cscript //nologo "%~f0?.wsf" %*` so that parameters get passed along to the script. – krowe Mar 19 '15 at 04:52
  • @krowe - That might make sense if the VBS example actually used the arguments. But you certainly can pass arguments to the CSCRIPT call. – dbenham Mar 19 '15 at 10:46
  • Change line 8 to read, `WScript.Echo "VBScript called by batch with " & WScript.Arguments.Count & " additional parameters."` ? – krowe Mar 19 '15 at 14:10
  • @jeb - Not an issue for this code, but `REM^ ` can fail if a file like `REM .BAT` exists. I think the same problem exists for `REM^,` and `REM^;`. I don't think there is a foolproof version for any situation. – dbenham Jan 14 '16 at 04:42
  • 1
    You are right, `REM^ ` fails. But `REM^, `, `REM^; ` and also `REM^: ` seem to be safe, I can't find a way that they fail and they still enable the possibility to use `&` in the same line – jeb Jan 14 '16 at 10:57
  • You're absolutely correct, my bad sorry -- I've been digging for JS solutions for too long and missed the original question. Thanks – Oleg Mihailik Nov 01 '16 at 18:00
  • 1
    @Paul - Regarding the Dec 28 comments concerning ` – dbenham Dec 30 '17 at 04:44
12

EDIT: My first answer was wrong for VBScript, now my next try...

Nice idea to use CTRL-Z, but I don't like control characters in a batch file, as it is problematic to copy&paste them.
It's depends of your browser, of your editor, of your ...

You could get a hybrid VBScript/Batch also with normal characters, and "normal" code.

:'VBS/Batch Hybrid
:
:
:'Makes the next line only visible for VBScript ^
a=1_
<1' echo off <nul 

set n=nothing' & goto :'batchCode & REM Jump to the batch code

'VBScript-Part
wscript.echo "VB-Start"
wscript.echo "vb-End"
wscript.quit(0)

'Batch part
:'batchCode
set n=n'& echo Batch-Start
set n=n'& echo two
set n=n'& echo Batch-End
set n=n'& cscript /nologo /E:vbscript vbTest.bat

The trick is to prepend each batch line with set n=n'& it is legal for both, but vbs will ignore the rest of the line, only batch will execute the rest of the line.

The other variant is :'remark ^, this is a remark for both, but for batch this remarks also the next line by the multiline character.
The VbScript sees then a=1<1 the rest of the line is a remark '
The batch sees only <1' echo off <nul, the first redirect from 1' will be override by the second <nul, so it results to only echo off < nul.

The only remaining problem is that you can see the first echo off, as it doesn't work in batch to use the @ after a redirection.

For JScript exists a simpler solution hybrid scripting

Community
  • 1
  • 1
jeb
  • 70,992
  • 15
  • 159
  • 202
  • I don't see how that helps for VBscript. Jscript and VBscript have totally different syntaxes. The real problem has been VBscript's lack of a multi-line comment. The VBscript won't compile if a single batch line is exposed anywhere in the script. – dbenham Jan 31 '12 at 12:15
  • You are right, didn't think enough about the vbscript problematic before, I will change my answer to a helpful variant – jeb Feb 01 '12 at 06:31
  • 1
    +1 Brilliant. Might be improved by adding `&SETLOCAL` after ECHO OFF. I would also move the n=nothing to the 2nd line and move the batch to the top with exit /b, but that is cosmetic. I still like the Ctrl-Z solution better because it supports @ECHO OFF, and because it doesn't set any extraneous variables. Ugly control chars vs. unwanted ECHO OFF output - we each have to choose our poison :-) – dbenham Feb 02 '12 at 03:12
3

And my attempt (first posted here).It resembles the jeb's solution ,but (at least according to me) the code is more readable:

:sub echo(str) :end sub
echo off
'>nul 2>&1|| copy /Y %windir%\System32\doskey.exe ".\'.exe" >nul

'& echo/ 
'& cscript /nologo /E:vbscript %~f0 %*
'& echo/
'& echo BATCH: Translation is at best an ECHO.
'& echo/
'& pause
'& rem del /q ".\'.exe"
'& exit /b

WScript.Echo "VBScript: Remorse is the ECHO of a lost virtue."
WScript.Quit

And the explanation:

  1. ' (single quote) is not forbidden symbol as a part of a file name in windows so there is no problem to have a file called '.exe
  2. There are few commands packed with windows that do nothing without command line parameters.The fastest (at doing nothing) and most light as a size (according to my tests) are subst.exe and doskey.exe
  3. So at the first line I'm coping the doskey.exe to '.exe (if it does not already exist) and then continue using it as '& (as the .exe extension should be in %PATHEXT%).This will be taken as a comment in VBScript and in batch will do nothing - just will continue with the next command on the line.

There are some flaws of course .At least for the first run eventually you will need administrator permissions as the coping in %windir%\System32\ might be denied.For robustness you can also use '>nul 2>&1|| copy /Y %windir%\System32\doskey.exe .\'.exe >nul and then at the end: '& rem del /q .\'.exe

With '.exe left in your path you could unintentially to use it in improper way , and the execution on every line of '.exe eventually could decrease the performance.

..And in the batch part commands must be only in one line.

Update 9.10.13 (..following the above convention)

Here's one more way which requires self-renaming of the batch file:

@echo off
goto :skip_xml_comment
<!--
:skip_xml_comment


    echo Echo from the batch

    ::start vbs code
    ( 
     ren %0 %0.wsf 
     cscript %0.wsf //nologo %*
     ren %0.wsf %0
    )


exit /b 0
-->

<package>
   <job id="vbs">
      <script language="VBScript">

         WScript.Echo "Echo from the VBS"

      </script>
   </job>
</package>

And a short explanation:

  1. When the batch is self-renamed and renamed again with the old name , and this is done in one line (or in brackets) the script will be executed with no errors.In this case is added also one call of CSCRIPT.EXE with .WSF file.Self-renaming is a little bit risky , but faster than a temp file.
  2. Windows script host does not care much for items outside of xml data as long as there are no special xml symbols & < > ; so @echo off and goto can be used without worries. But for safety it's good to put batch commands in xml comment (or CDATA ) section .To avoid error messages from the batch I've skipped the <!-- with goto here.
  3. Before closing the xml comment batch script is terminated with exit

So the good things are that there's no need to write all batch code in single line commands, there's no annoying echo off displayed , jscript code and different jobs could be added to the script.Also no need of special symbols and creation of additional exe files like '.exe

On the other hand self-renaming could be risky.

npocmaka
  • 51,748
  • 17
  • 123
  • 166
3

I have tried to assemble all solutions in one script at http://www.dostips.com/forum/viewtopic.php?p=37780#p37780.

There is the batch script converting the most popular languages into a batch file (.js, .vbs, .ps1, .wsf, .hta and historical .pl).

It works as follows:


    :: convert filename1.vbs to executable filename1.bat
    cmdize.bat filename1.vbs

jsxt
  • 1,002
  • 9
  • 23
3

There is a very simple solution for this problem. Just use an Alternate Data Stream (ADS) to store the VBS code. To explain it simply, an ADS is another place where you can store other data in the same file, that is, a file is comprised of its original data plus any number of additional ADS's. Each ADS is identified separating its own name from the original file name via a colon. For example:

test.bat                    <- a file called "test.bat"
test.bat:script_.vbs        <- an ADS of file "test.bat" called "script_.vbs"

You may find a more technicall and extensive description of ADS's in the web, but it is important to mention now that this feature works on NTFS disks only. Now, lets see how use this feature to solve this particular problem.

First, create the original Batch file in the usual way. You may also start notepad.exe from the command line this way:

notepad test.bat

For my example, I inserted the following lines in test.bat:

@echo off
setlocal

For /f "delims=" %%i in ('Cscript //nologo "test.bat:script_.vbs" "Select a folder"') do Set "folder=%%i"

echo Result: "%folder%"

Note the name of the VBS script. After test.bat was saved and Notepad closed, create the VBS script using Notepad, so enter this command at the command-line:

notepad test.bat:script_.vbs

And create the script in the usual way:

Dim objFolder, objShell
Set objShell = CreateObject("Shell.Application")
Set objFolder = objShell.BrowseForFolder(0, "Select a folder.", &H4000, 0)
If Not (objFolder Is Nothing) Then
   wscript.echo objFolder.Self.path
Else
   wscript.echo 0
End If

Save this file and run test.bat. It works! ;)

You may review the ADS's of test.bat file via the /R switch in dir command. You may read a more extensive explanation of this method and its limitations at this post.

Aacini
  • 59,374
  • 12
  • 63
  • 94
0

I don't how much exactly the VB.NET counts for VBScript but it is definitely VisualBasic code with some C# flavours.

With the version of msbuild there's a thing called an inline tasks that allows you to load a .net code into memory and execute it. And this can be embedded into a batch file with the a similar manner that used with wsf:

<!-- :
        @echo off
        echo -^- FROM BATCH

        set "CMD_ARGS=%*"
        :::::::::::::::::  Starting VB.NET code :::::::::::::::::::::::
        ::
        :: searching for msbuild location
        for /r "%SystemRoot%\Microsoft.NET\Framework\" %%# in ("*msbuild.exe") do  set "msb=%%#"

        if not defined  msb (
           echo no .net framework installed
           exit /b 10
        )

        ::::::::::  calling msbuid :::::::::
        call %msb% /nologo  /noconsolelogger "%~dpsfnx0"
        ::
        ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
        exit /b %errorlevel%
      
--> 


<Project ToolsVersion="$(MSBuildToolsVersion)" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Target Name="T">
    <T/>
  </Target>
  <UsingTask
    TaskName="T"
    TaskFactory="CodeTaskFactory"
    AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v$(MSBuildToolsVersion).dll" > 
    <Task>
     <!-- include this for UI programs -->
     <!-- Reference Include="$(MSBuildToolsPath)\System.Windows.Forms.dll"/ -->
      <Using Namespace="System"/>
      <Code Type="Fragment" Language="vb">
        <![CDATA[
            '''' VB CODE STARTS HERE ''''
            
            Console.WriteLine("-- FROM VB.NET"):
            
            '''''''''''''''''''''''''''''
        ]]>
      </Code>
    </Task>
  </UsingTask>
</Project>

it verbose indeed though xml syntax allows almost everything to be put in one line if somebody wants (but I thing for educational reasons is better this example to be left like that). The batch code cannot contain -- so for robustness this part can be put in a CDATA also.

Other way to embed VB.NET into batch without temp files is with the help of powershell and its type definition cmdlet:

<# : batch portion
@echo off & setlocal

set "CMD_ARGS=%~1"

powershell -noprofile "iex (${%~f0} | out-string)"
goto :EOF

: end batch / begin powershell #>

param($psArg1 = $env:psArg1)


$VB = @" 
Imports System
namespace VB 
    Public Class VB
        Public Shared Sub Print()
            Console.WriteLine("PRINTED BY VB")
        End Sub
    End Class
End Namespace
"@

Add-Type -TypeDefinition $VB -Language VisualBasic


[VB.VB]::Print()
npocmaka
  • 51,748
  • 17
  • 123
  • 166