Powergui中有个工具,可以将PowerShell脚本转换成独立的可执行程序EXE。所以,我想写一个PowerShell函数,能够将一个Ps1脚本文件转换成同名的可执行文件。

知识点分析

  • 关键应当使用到.Net动态编译类Microsoft.CSharp.CSharpCodeProvider。在内存中编译,输出为可执行程序EXE。

  • 编译不通过时,输出编译错误信息,包含行号和列号;编译通过时,输出应用程序路径。

  • 将要编译的脚本作为Resource文件嵌入到目标应用程序中。

  • 为了确保最大的兼容性,不适用c#执行PowerShell脚本,直接使用Process打开PowerShell.exe ,将脚本文件存到临时目录传递过去运行。

  • 将Process运行过程中产生的标准输出,异步重定向应用程序。

源脚本(Convert-PS1ToExe.ps1)

function Convert-PS1ToExe
{
     param (
     [ Parameter ( Mandatory = $true )]
     [ValidateScript({ $true })]
     [ValidateNotNullOrEmpty()]   
     [IO.FileInfo] $ScriptFile
     )
     if -not $ScriptFile .Exists)
     {
         Write-Warning "$ScriptFile not exits."
         return
     }
 
     [string] $csharpCode @'
     using System;
     using System.IO;
     using System.Reflection;
     using System.Diagnostics;
     namespace LoadXmlTestConsole
     {
         public class ConsoleWriter
         {
             private static void Proc_OutputDataReceived(object sender, System.Diagnostics.DataReceivedEventArgs e)
             {
                 Process pro = sender as Process;
                 Console.WriteLine(e.Data);
             }
             static void Main(string[] args)
             {
                 // Set title of console
                 Console.Title = "Powered by PSTips.Net";
 
                 // read script from resource
                 Assembly ase = Assembly.GetExecutingAssembly();
                 string scriptName = ase.GetManifestResourceNames()[0];
                 string scriptContent = string.Empty;
                 using (Stream stream = ase.GetManifestResourceStream(scriptName))
                 using (StreamReader reader = new StreamReader(stream))
                 {
                     scriptContent = reader.ReadToEnd();
                 }
 
                 string scriptFile = Environment.ExpandEnvironmentVariables(string.Format("%temp%\\{0}", scriptName));
                 try
                 {
                     // output script file to temp path
                     File.WriteAllText(scriptFile, scriptContent);
 
                     ProcessStartInfo proInfo = new ProcessStartInfo();
                     proInfo.FileName = "PowerShell.exe";
                     proInfo.CreateNoWindow = true;
                     proInfo.RedirectStandardOutput = true;
                     proInfo.UseShellExecute = false;
                     proInfo.Arguments = string.Format(" -File {0}",scriptFile);
 
                     var proc = Process.Start(proInfo);
                     proc.OutputDataReceived += Proc_OutputDataReceived;
                     proc.BeginOutputReadLine();
                     proc.WaitForExit();
                     Console.WriteLine("Hit any key to continue...");
                     Console.ReadKey();
                 }
                 catch (Exception ex)
                 {
                     Console.WriteLine("Hit Exception: {0}", ex.Message);
                 }
                 finally
                 {
                     // delete temp file
                     if (File.Exists(scriptFile))
                     {
                         File.Delete(scriptFile);
                     }
                 }
 
             }
 
         }
     }
'@
 
     # $providerDict
     $providerDict New-Object 'System.Collections.Generic.Dictionary[[string],[string]]'
     $providerDict .Add( 'CompilerVersion' , 'v4.0' )
     $codeCompiler [Microsoft.CSharp.CSharpCodeProvider] $providerDict
 
     # Create the optional compiler parameters
     $compilerParameters New-Object 'System.CodeDom.Compiler.CompilerParameters'
     $compilerParameters .GenerateExecutable =  $true
     $compilerParameters .GenerateInMemory =  $true
     $compilerParameters .WarningLevel = 3
     $compilerParameters .TreatWarningsAsErrors =  $false
     $compilerParameters .CompilerOptions =  '/optimize'
     $outputExe Join-Path $ScriptFile .Directory  "$($ScriptFile.BaseName).exe"
     $compilerParameters .OutputAssembly =   $outputExe
     $compilerParameters .EmbeddedResources.Add( $ScriptFile .FullName) >  $null
     $compilerParameters .ReferencedAssemblies.Add(  [System.Diagnostics.Process] .Assembly.Location ) >  $null
 
     # Compile Assembly
     $compilerResult $codeCompiler .CompileAssemblyFromSource( $compilerParameters , $csharpCode )
 
     # Print compiler errors
     if ( $compilerResult .Errors.HasErrors)
     {
         Write-Host 'Compile faield. See error message as below:' -ForegroundColor Red
         $compilerResult .Errors |  foreach {
             Write-Warning ( '{0},[{1},{2}],{3}' -f $_ .ErrorNumber, $_ .Line, $_ .Column, $_ .ErrorText )
         }
     }
     else
     {
          Write-Host 'Compile succeed.' -ForegroundColor Green
          "Output executable file to '$outputExe'"
     }
}


用法为

Convert-PS1ToExe -ScriptFile .\second.ps1