One may wonder - how to make a .bat file with embedded c# code to compile and execute it 'on-the-fly'?
Is it possible to have both batch-instructions and c# code in one file?
3 Answers
Yes, it is possible.
Here is an example:
Example.bat
/* 2> nul
@echo off && cls && echo Loading... && echo.
set WinDirNet=%WinDir%\Microsoft.NET\Framework
if exist "%WinDirNet%\v2.0.50727\csc.exe" set csc="%WinDirNet%\v2.0.50727\csc.exe"
if exist "%WinDirNet%\v3.5\csc.exe" set csc="%WinDirNet%\v3.5\csc.exe"
if exist "%WinDirNet%\v4.0.30319\csc.exe" set csc="%WinDirNet%\v4.0.30319\csc.exe"
if "%csc%" == "" ( echo .NET Framework not found! && echo. && pause && exit )
%csc% /nologo /out:"%~dpnx0.exe" "%~dpnx0"
if not "%ERRORLEVEL%" == "0" ( echo. && pause && exit )
cls
"%~dpnx0.exe" %*
del "%~dpnx0.exe"
exit
*/
using System;
class Program
{
static void Main()
{
System.Console.WriteLine("Hello, World!\r\nI am at " + System.Environment.Version);
}
}
Explanation: this batch-file consist of two parts: firstly a batch-code and secondary a c# code. When executing, command shell will ignore c#-comments /* and */ as error-lines and execute only batch-code. Due exit command at the end of batch-block, the execution never reach c# code.
Batch part of the file searches for csc.exe (.NET compiler). After found, batch file passes itself into csc.exe to compile c# code. Due to comments (/* and */) batch-part is ignored and only c# part will be compiled. After compilation generated .exe file is executed and deleted after execution.
Edit: 2> nul redirecting standard error (descriptor 2) to null to suppress 'not-found' message.
5 Comments
'/*' is not recognized as an internal or external command, operable program or batch file. whenever you start the batch file, though :pcls is supposed to take care of that :p/* 2> nul as first line will suppress the "not found" message: stackoverflow.com/q/11060166/395685 Sadly, since this is before the @echo off, it won't suppress the command showing though, so it'll still need the cls.@echo off, meaning it'll appear on screen. And you can't put an @ before it either, since that'll once again make it C#-invalid. But at least you won't worry users with an error message flashing on their console for a split second, I guess.You can comment out the batch code, but you can't avoid that the comment is handled by batch as an error.
The error itself can be suppressed, but at least the first line will be shown.
/* 2>NUL
@echo off
...
start the C# code
*/
...
C#-Code
As C# seems not to accept one of the :@ characters as the first character in a file, I can't see a possible way for a perfect solution.
Comments
There are few ways.
1) Similar to the previous answers ,but with automatic detection the latest version of .net framework:
// 2>nul||@goto :batch
/*
@echo off
setlocal
:: find csc.exe
set "csc="
for /r "%SystemRoot%\Microsoft.NET\Framework\" %%# in ("*csc.exe") do set "csc=%%#"
if not exist "%csc%" (
echo no .net framework installed
exit /b 10
)
if not exist "%~n0.exe" (
call %csc% /nologo /w:0 /out:"%~n0.exe" "%~dpsfnx0" || (
exit /b %errorlevel%
)
)
%~n0.exe %*
endlocal & exit /b %errorlevel%
*/
using System;
class QE {
static void Main(string[] args) {
Console.WriteLine("Echo from C#");
}
}
2) In windows 10 the .net framework is installed by default and there the msbuild allows inline tasks which gives a possibility for in-memory compilation (i.e. - no additional exe files) and emebedding in xml which means no redundant output. Note that command line arguments are saved in CMD_ARGS environment variable:
<!-- :
@echo off
echo -^- FROM BATCH
set "CMD_ARGS=%*"
:::::: Starting C# 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
)
rem :::::::::: calling msbuid :::::::::
call %msb% /nologo /noconsolelogger "%~dpsfnx0"
rem ::::::::::::::::::::::::::::::::::::
exit /b %errorlevel%
-->
<Project ToolsVersion="$(MSBuildToolsVersion)" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="_">
<_/>
</Target>
<UsingTask
TaskName="_"
TaskFactory="CodeTaskFactory"
AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v$(MSBuildToolsVersion).dll" >
<Task>
<Reference Include="$(MSBuildToolsPath)\System.Windows.Forms.dll"/>
<Using Namespace="System" />
<Code Type="Method" Language="cs">
<![CDATA[
public override bool Execute(){
MyMethod();
return true;
}
void MyMethod(){
Console.WriteLine("Whoa");
String CMD_ARGS=Environment.GetEnvironmentVariable("CMD_ARGS");
System.Console.WriteLine("Echo from C# with msBuild -- "+"$(MSBuildToolsVersion)");
}
]]>
</Code>
</Task>
</UsingTask>
</Project>
3) With powershell and Add-Type:
<# : 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)
$CS = @"
namespace PS {
public class CS
{
public static void csEcho(string arg)
{ System.Console.WriteLine("echo from C# " + arg); }
}
}
"@
Add-Type -TypeDefinition $CS -Language CSharp
[PS.CS]::csEcho($psArg1 + " and PowerShell")
4) One more note - if you're not using P/Invoke may more convenient will be to use JScript.net which require less code and has similar syntax and give you access to the .net stuff and has no redundant output:
@if (@X)==(@Y) @end /* JScript comment
@echo off
setlocal
for /f "tokens=* delims=" %%v in ('dir /b /s /a:-d /o:-n "%SystemRoot%\Microsoft.NET\Framework\*jsc.exe"') do (
set "jsc=%%v"
)
if not exist "%~n0.exe" (
"%jsc%" /nologo /out:"%~n0.exe" "%~dpsfnx0"
)
%~n0.exe %*
endlocal & exit /b %errorlevel%
*/
import System;
Console.Write("Echo from .NET")