DOS BAT magic

::set - {

 @goto start

Richard Suchenwirth - In exec magic, it was explained how a script can be understood both by a Unix shell (which starts tclsh with itself) and Tcl. Here's my version of how to achieve the same with MS-DOS batch files.

The first line (up above) is the first trick - to DOS it's an acceptable label because of the leading colon (:), so it proceeds to the :start label (you can omit that section, it's only to document it in Wiki style ;-) The commands after that turn off the echoing of command input and possibly complete the filename with the .bat extension. But you still have to call this script with relative path name - BAT files are searched on PATH but don't tell you where they were found. Then tclsh is called with the name of the script and up to 9 arguments, which you may group in DOS with double quotes if they contain whitespace or are empty strings, and after that control is transfered to the again fancily named label "end of BAT file", where the script ends without terminating its caller (which DOS's exit would do).

To Tcl, the global set command assigns the content of the braces to the variable "-" which is unset immediately after. From that line, your "payload" code may begin. In it, you may refer to environment variables that were set in the DOS part of the script - e.g ::env(script) or ::env(SCRIPT) (case doesn't matter that much for DOS) also contains the script's file name.

Finally, the label "end of BAT file" is hidden to Tcl by prolonging the preceding comment by the trailing backslash (which must not be omitted, like in exec magic).

For a linguist, twisting a single source file so that three very diverse languages (DOS-BAT, Tcl, and WikiML) understand it each in their own way, makes a small but entertaining weekend fun project...enjoy!


 :start
 @echo off
 set script=%0
 if exist %script%.bat set script=%script%.bat
 tclsh %script% %1 %2 %3 %4 %5 %6 %7 %8 %9
 goto end of BAT file
 };unset - ;#-------------------------------begin "payload" Tcl
 puts "Hello, DOS, time now is"
 puts [clock format [clock seconds]] 
 gets stdin line ;# just to test 
 puts [string toupper $line]
 puts [list argc: $argc argv: $argv]
 #------------------------------------------end Tcl\
 :end of BAT file

My god, Richard. You have brought back long supressed memories of multi-hundred line DOS batch files... -PSE

unperson Good stuff never gets suppressed from one's memory! It just remains and comes back one day when it is needed!

IL hmm, wow. my jaw is pretty dropped, i think this is the first time i've been excited about bat since my ibm pc jr

mmm yeah good old bat files - JS


KPV The folklore idiom for this is below. Don't remember where I first saw it--I couldn't find it in the FAQ just now--but it's been around for a while. This is fairly barebones and doesn't handle the missing .bat extension: if your using 4Dos you can replace %0 with %@search[%0], otherwise you can use RS's if exist... logic from above.

 ::catch {};#\
 @tclsh.exe %0 %1 %2 %3 %4 %5 %6 %7 %8 %9
 ::catch {};#\
 @goto eof

unperson For me you have brought the realization that some things work great and are tremendously useful like DOS batch files. Microsoft was clever enough to keep them around. I just wish they could do more like open a file; show files in a sub-directory etc. If you can figure that one out, Richard, write a book and you'll be a rich man! You'd be surprised at the number of people who still use DOS nowadays! And some of them are pretty young!

Richard, what does the batch file on top do anyway? - RS: It does what is between "payload" and "end Tcl" - up to "end of BAT file" it is one file.

--- Randolf Schultz Does the automatic .bat extension addition work on WinXP? I think no, because on WinXP %0 is always replaced with quotes, so that the "if exists"-line above does in fact (on WinXP) the following: if exists "myscript".bat set script="myscript".bat Could someone with XP please try and comment, I am not sure, whether this really affects the if exists... If it does not work, http://www.ayam3d.org/ayam.bat has some additional magic to cure this (use %~0)

    if exist "%script:"=%.bat" set script="%script:"=%.bat" 

removes existing "'s around the root name then quotes the whole file name to handle spaces -- NT4+ cmd.exe only! NOT DOS. May also handle the path problem below. -- AET.

--- Randolf Schultz What happens if the script is located in a path with spaces, like e.g. "Documents and Preferences"? There should be quotes around the %script% in the tclsh call (and perhaps around the %1 too, in case our script is not called manually, but via drag and drop, which in turn may not add quotes...).

DKF: Something like this might also be workable.

 ::if 0 {
 @tclsh.exe %0 %1 %2 %3 %4 %5 %6 %7 %8 %9
 @goto eof
 }

MNP: Here's the Tcl version of the DOS part of the results of perl2bat. However, thanks Donal because your use of 'goto :eof' improves upon their version yielding:

 ::if 0 {--*-tcl-*--
 @echo off
 if "%OS%" == "Windows_NT" goto WinNT
 tclsh "%0" %1 %2 %3 %4 %5 %6 %7 %8 %9
 goto :eof
 :WinNT
 tclsh %0 %*
 if NOT "%COMSPEC%" == "%SystemRoot%\system32\cmd.exe" goto :eof
 if %errorlevel% == 9009 echo You do not have Tclsh in your PATH.
 if errorlevel 1 goto script_failed_so_exit_with_non_zero_val 2>nul
 goto :eof
 }
 # start Tcl code here

Tested on Windows XP SP2 only.


EKB I had to cut out the Wiki-digestible part of RS's code above before I was sure I'd understood it. In case there are other folks who have trouble holding more than two languages (DOS-BAT, Tcl, and WikiML) in their head at once, here's a stripped-down version of the code at the top of the page. (I also replaced the example code with a simple old "Hello, World"). All code is contained in one nice little gray box:

 ::set - {
   @echo off
   set script=%0
   if exist %script%.bat set script=%script%.bat
   tclsh %script% %1 %2 %3 %4 %5 %6 %7 %8 %9
   goto end of BAT file
 };unset - ;#-------------------------------begin "payload" Tcl
 # My tclsh script

 puts "Hello, world"

 # Wait for Return to close the window
 gets stdin test

 #------------------------------------------end Tcl\
 :end of BAT file

Matthias Hoffmann Here's my version, adapted from the examples above and simplified (stripped off paths etc.):

 ::catch {};#\
 tclsh84.exe %~n0.bat %*
 ::catch {};#\
 @goto :EOF

So what's the difference at all?

  • using %* means that you are no longer limited to a maximum of 9 parameters (or forced to use the shift command...)
  • using %~n0.bat means: the extension is first stripped off (%~n0) and added afterwords as .bat. the reason for this is that I've noticed that sometimes the extension is append to %0 and sometimes is not...

NT's (and w2k's and XP's) cmd.exe is not as dumb as it seems.... it has nearly nothing to do anymore with the command.com from the old DOS-days except keeping a good level of backward-compatibility. Nevertheless Microsoft has noticed after years that cmd.exe isn't good enough: they want to give us another new shell (I forgot the name) in the next OS-Release (already ready for beta testing). Every few years the wheel is reinvented again - isn't it funny? :-)

AET 03feb06 I put my script.bat into my \bin folder - which is in my PATH, so I can invoke it from anywhere. The above expression however can't find the file, 'cos it gets the filename (script.bat) only. So I used this successfully; it expands to the fully qualified path:

 ::catch {};#\
 @start wish "%~f0" & goto :eof

The cmd console is released immediately while the tcl script does it's thing.

You can use call instead of start where you want the tcl script to return stdout to the .bat script.

You can use %~dp0\%~n0.bat instead of %~f0 if you have the occasional problem outlined by Matthias above.

And of course, add %* if you expect args.

 @for /f "tokens=*" %%i in ("%~f0") do wish "%%i" & goto :eof

also works, though I can't immediately think of a reason to use this.

Note the double quotes to cope with spaces in the path. I found that you MUST have exit at the end of your tcl script, otherwise control never returns to the .bat file after the call. This can be an additional problem if you use start, cos you end up with an idle wish process left over every time it is used.


Here's a challenge: make it work on unix, too. I'd like to be able to dump a script on to a flash drive and be able to double-click on it no matter whether it's mounted on a unix or windows system. Is that possible, to have something that is altogether bash, BAT and wish friendly?

slebetman: That's quite easy, something like this:

    #! /usr/bin/env wish
    ::if 0 {
        start wish "%~f0" %*
        exit
    }
    pack [button .x -command exit -text Exit] -fill both

works on both Windows and Unix. Oh sure, the COMSPEC shell will complain about '#!' not being a recognised command but it will just happily continue executing the next line after complaining ;-)

KPV: The above script doesn't work when invoked from a dos command window--the exit command causes the windows to close. Perhaps using goto eof instead would work better.

JFL: I compiled a trivial C program called #!.c that does nothing, then put #!.exe in the path. This prevents the unrecognized command error that you had. An interesting extension would be to actually use #!.exe to launch its parent script using the correct Windows interpreter. This requires getting the parent process command line arguments, which is a difficult thing to do in Windows. I'm experimenting with this, but am having difficulties. I'll post an update here when I get results.

APN: The following will get the parent's command line if you are willing to use TWAPI. However, it may not work if the parent is running under different account/privileges.

twapi::get_process_info [lindex [twapi::get_process_info [pid] -parent] 1] -commandline

PR: A very useful thing to do with BAT files are Droplets.


n0nsense: What about this (with no spaces in front of any line starting with a colon):

 ::catch {
 wish84 %*
 rem you can put any other MS-BAT code between "{" and "goto :eof" ...
 goto :eof
 rem }
 # TCL CODE ----------

 puts "Hello World!"; # etc...

 # END OF TCL CODE ---
 # it could be necessary to write ":eof" into the last line if you want to run it in DOS...
 # I don't know but I think Microsoft had the "goto eof" possibility first time in W2k...?
 ::catch {}; #\
 :eof

wdb My version uses a batchfile called #!.bat with this content:

 @ echo off
 if exist %2 %1 %2 %3 %4 %5 %6 %7 %8 %9
 if exist %2.bat %1 %2.bat %3 %4 %5 %6 %7 %8 %9

Having the command #! in my path, I can write a batchfile for Tcl, e.g. hello.bat:

 #! c:\Tcl\bin\tclkitsh85.exe %0
 puts "hello world"

This version does not need any tricky code at end, because if a batch file calls another one, the calling batch file is terminated. (Think of it as a jmp, not jsr.)


gasty - 2009-07-07 14:43:44

This is my version:

::if no {
  @tclkitsh "%~f0" %*
  @goto :eof 
}
# Tcl code start

puts "Filename: $argv0"
puts "Arguments: $argv"

# Tcl code end \
:eof

BMA You actually don't need the :eof label, as the command language interpreter assumes that :eof means the end of file

::if no {
  @tclkitsh "%~f0" %*
  @goto :eof 
}
# Tcl code start

puts "Filename: $argv0"
puts "Arguments: $argv"