Batch Files
People usually smile when I say that some parts of our network like the filtering of system events are held together by batch files. It just seems so arcane, but there are some big benefits:
You don’t have to compile anything, you always know where the source code is and you can simply copy them between machines without having to set up anything. And since it’s more or less a legacy technology, Microsoft isn’t really changing a lot anymore, so there’s little chance of an upgrade breaking a batch file.
The only problem is: cmd.exe shell syntax is a horrible, horrible mess and even the most basic string functions can take ages to implement… plus, the code you’ll write look like gibberish to anybody else no matter what you do. Plus there’s the horrible, horrible string escaping behavior and the very strange behavior of variables.
PowerShell
So, Microsoft started developing a replacement: PowerShell.exe . And functionality-wise it’s wonderful… it can be run interactively, it doesn’t need compilation, it has useful variables, it can access the system’s .NET libraries… it all sounds wonderful… until you try to run the darn thing. Let’s just say: The syntax is frighteningly bad, never mind the documentation plus the fact that for some bizarre reason you’re allowed to run batch files or EXE files, but you need to set an additional policy before you’re allowed to run PowerShell scripts!
The C# Compiler
But enough ranting. Thankfully there’s an alternative that’s preinstalled on all modern Windows System: The C# compiler. Yes, it’s there, even if you don’t have VisualStudio installed. Just enter
dir “%WINDIR%\Microsoft.NET\Framework\v4*”
on the commandline and you’ll see the directory of all installed .NET 4 frameworks, each containing CSC.EXE, which is the C# compiler.
Now, you could just use that, but that means a whole lot of temp files since you can’t pipe to CSC.EXE and you can’t run the code immediately. However there’s another way to access it: Through .NET itself via System.CodeDom.Compiler.CodeDomProvider .
Using PowerShell to access the C# Compiler
Thankfully, there’s one thing that PowerShell gets right: Giving you access to .NET . It’s not a pleasant experience, but it is possible. And there’s another thing PowerShell gets right: it allows piping anything to it. So we can use a little PowerShell script that invokes CodeDomProvider.CreateProvider to compile our code on the fly and run it immediately.
It’s really pretty simple:
$opt = New-Object System.CodeDom.Compiler.CompilerParameters; $opt.GenerateInMemory = $true; $cr = [System.CodeDom.Compiler.CodeDomProvider]::CreateProvider ("CSharp").CompileAssemblyFromSource($opt, "public class App { public static void Main() { "+ $input+" } }"); if($cr.CompiledAssembly){ $obj = $cr.CompiledAssembly.CreateInstance("App"); $obj.GetType().GetMethod("Main").Invoke($obj, $null); }else{ $cr.errors; }
It’s really very straight forward. Take STDIN, wrap it in a Main function, compile it, run it, report error if there was one during compilation.Through the magic of horrible cmd.exe paramter escaping, this looks a bit differently when passed directly to PowerShell.exe (3 quotes), but you should still be able to recognize it. Just put it in any old batch file (I’m using c#.cmd which I also added to my system’s PATH variable so that I don’t have to enter the whole path each time), but be sure to put it in a single line, because even escaping the linebreak with “^” won’t work for arguments of PowerShell.exe :
@PowerShell -Command " $opt = New-Object System.CodeDom.Compiler. CompilerParameters; $opt.GenerateInMemory = $true; $cr = [System. CodeDom.Compiler.CodeDomProvider]::CreateProvider("""CSharp"""). CompileAssemblyFromSource($opt,"""public class App { public static void Main() { """+ $input+""" } }"""); if( $cr.CompiledAssembly) {$obj = $cr.CompiledAssembly. CreateInstance("""App"""); $obj.GetType().GetMethod("""Main"""). Invoke($obj, $null);}else{ $cr.errors; } "
Horrible, I know. But it works.
Including C# inline in batch files
Now, if you want to actually include any C# in your batch file, it’s surprisingly straight-forward since the cmd.exe ECHO command actually has very straight forward escaping rules. Well, except for | and & , which you best avoid by using .Equals() . But new lines just need to be escaped with a “^” at the end of the line and a space before the final pipe character. OK, that sounds way worse than it actually is:
@echo ^ var a="Hello";^ var b="World";^ var foo=(a+" "+b).ToUpper();^ System.Console.WriteLine(foo);^ if(System.IO.File.Exists("C#.cmd")){^ System.Console.WriteLine("Hey, you named it C#.cmd too :)");^ }^ |c#
That’s what a typical call would look like. Again, note the “^” at the end of each line and the space before “|c#”. Remember this and you will be fine. Of course, you can also put the CSharp code in a separate file and use @TYPE to pipe it directly to C#.CMD, so it won’t need any escaping.
Issues
Well, there’s obviously the issue of escaping your code if you use ECHO to include it inline, but I really don’t think there’s any way to avoid it.
There are some issues which are mostly due to the C# code running inside the PowerShell process, rather than the CMD.EXE process. Most importantly: You cannot set environment variables without setting them user- or system-wide. You can set the environment variables of the PowerShell process, but these won’t be visible to the parent CMD.EXE process either. Your only way out is to use STDOUT and STDERR and FOR /F to move it to a variable. If that doesn’t work (which may be the case if you want to include the code inline, because escaping inside a CMD.EXE FOR call is incredibly difficult), you’ll need to transport the information using the filesystem.
And since we’re piping the code to PowerShell, STDIN will obviously not be available… so no ReadLine().
TODO
Well, obviously support for commandline arguments would be nice at some point, but I haven’t needed it so far.
It would also be nice if the PowerShell could add the class/main wrapper only if there is no method given in the source code. For now I’m simply using two different batch files, c#.cmd and c#full.cmd
Hopefully this will make your life a bit easier 🙂