Write-Program.ps1

#requires -version 2.0
function Write-Program
{
    <#
    .Synopsis
        Creates an application to run a PowerShell command.
    .Description
        Creates an application to run a PowerShell command.
         
        The application will wrap any cmdlet and any function stored in a module and create a .exe
         
        The .exe will support all of the same parameters of the core cmdlet.
         
        You can use this to allow users to simply double click a script, rather than making a user import a module manually.
         
        All of the text output from the EXE will be shown when the EXE is complete. Streaming is not yet supported.
    .Example
        Write-Program -Command Get-Process
         
        .\Get-Process.exe
         
        .\Get-Process.exe -Name powershell*
    .Link
        Add-Type
    #>

    [CmdletBinding(DefaultParameterSetName='ScriptBlockExe')]
    param(
    # The name of the command you're wrapping
    [Parameter(Mandatory=$true,
        ParameterSetName='CommandExe',
        ValueFromPipelineByPropertyName=$true)]
    [Alias('Name')]
    [String]$Command,

    # The script block that forms the core of the program
    [Parameter(Mandatory=$true,
        ParameterSetName='ScriptBlockExe',
        Position=0,
        ValueFromPipelineByPropertyName=$true)]
    [ScriptBlock]
    $ScriptBlock,

    
   
    
    
    # Functions to embed
    [ScriptBlock]$FunctionsToEmbed,
    
    # the path the command is outputting to
    [string]$OutputPath,
    
    # If this is set, the command will be a windows application.
    # It will no longer display help or errors, but PowerShell can continue while it is running
    [switch]$WindowsApplication,
    
    # If set, this will keep the Program Debug Database (PDB file) generated by Add-Type.
    #
    # Otherwise, this value will be thrown away
    [switch]$KeepDebugInformation,
    
    # If set, will embed functions to create the exe.
    [switch]$Embed,
        
    # Outputs the C# code for the .exe
    [switch]$OutputCSharp,
    
    # Outputs the script containing the C# code and the Add-Type command
    [switch]$OutputScript
    )
    
    process {
        
        #region Resolve Command & Check for Command Type
        if ($psCmdlet.ParameterSetName -eq 'ScriptBlockExe') {
            
        } else {

        
        $commandName = $command
        $realCommand = Get-Command $commandName | 
            Select-Object -First 1 
        if (-not $realCommand) {
            Write-Error "$command Not Found"
            return
        }        
        if ($realCommand -isnot [Management.Automation.FunctionInfo] -and
            $realCommand -isnot [Management.Automation.CmdletInfo] -and
            $realCommand -isnot [Management.Automation.ExternalScriptInfo]) {
            Write-Error "Cannot create programs that are not in a module on disk"
            return
        }
        }
        #endregion Resolve Command & Check for Command Type
        


        if ($Embed -or $realCommand -is [Management.Automation.ExternalScriptInfo]) {
            $sb = [ScriptBlock]::Create("$realCommand")
            $embeddedCommands = foreach ($cmd in Get-ReferencedCommand -ScriptBlock $sb) {
                if ($cmd.Module) {
                    Write-Warning "$cmd comes from $($cmd.Module). When commands are embedded, the module will not be imported."                    
                }
                if ($cmd -isnot [Management.Automation.FunctionInfo]) 
                {
                    continue    
                }
"function $($cmd.Name) {
$($cmd.Definition)
}"

                
            }
            
            $embeddedCommandsText = $embeddedCommands -join [Environment]::NewLine -replace '"', '""'
        } else { 
            if ($psCmdLet.ParameterSetName -ne 'ScriptBLockExe') {
                if ($realCommand.Module -and -not $realCommand.Module.Path) {
                    Write-Error "Cannot create programs that are not in a module on disk"
                    return
                }
            }
        }
        
        $namespaces = '
using System;
using System.Collections;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
'


        if ($windowsApplication) {
            $namespaces += '
using System.Windows;
using System.Windows.Controls;
'

        }

        $code = $namespaces + @'
 
public class Program {
    public static void Main(string[] args) {
        Collection<Object> arguments = PowerShell
            .Create()
            .AddScript(@"
$positionalParameters = New-Object Collections.ObjectModel.Collection[string]
$namedParameters = @{}
$args = @($args)
for ($i =0 ; $i -lt $args.Count; $i++) {
    if ($args[$i] -like '-*') {
        # Named Value
        if ($args[$i] -like '-*:*') {
            # Coupled Named Value
            $parameter = $args[$i].Substring(1, $args[$i].IndexOf(':') - 1)
            $value = $args[$i].Substring($args[$i].IndexOf(':') + 1)
            $namedParameters[$parameter] = $value
        } elseif ($args[$i + 1] -notlike '-*') {
            # The next argument is the value
            if (-not ($args[$i + 1])) {
                # Really a switch, because there is no additional argument
                $namedParameters[$args[$i].Substring(1)] = $true
            } else {
                $namedParameters[$args[$i].Substring(1)] = $args[$i + 1]
                $i++ # Incremenet $i so we don't end up reusing the value
            }
        } else {
            # Assume Switch
            $namedParameters[$args[$i].Substring(1)] = $true
        }
    } else {
        # Assume Positional Parameter
        $positionalParameters.Add($args[$i])
    }
}
$positionalParameters
$namedParameters")
    .AddParameters(args)
    .Invoke<Object>();
     
     
    IDictionary namedParameters = null;
    StringCollection positionalParameters = new StringCollection();
    for (int i = 0; i < arguments.Count; i++)
    {
        if (arguments[i] is IDictionary)
        {
            namedParameters = arguments[i] as IDictionary;
        }
        if (arguments[i] is string)
        {
            positionalParameters.Add(arguments[i] as string);
        }
    }
'@



        $code += @"
        Runspace rs = RunspaceFactory.CreateRunspace();
        rs.ApartmentState = System.Threading.ApartmentState.STA;
        rs.ThreadOptions = PSThreadOptions.ReuseThread;
        rs.Open();
 
        PowerShell powerShellCommand = PowerShell.Create()
            .AddCommand("Set-ExecutionPolicy")
            .AddParameter("Scope","Process")
            .AddParameter("Force")
            .AddArgument("Bypass");
        powerShellCommand.Runspace = rs;
        powerShellCommand.Invoke();
        powerShellCommand.Dispose();
         
 
"@
    
        
        if ($psCmdlet.ParameterSetName -eq 'ScriptBlockExe') {

$code += @"
            powerShellCommand = PowerShell.Create()
                .AddScript(@"$($ScriptBlock.ToString().Replace('"', '""'))");
            powerShellCommand.Runspace = rs;
 
            if (namedParameters != null) {
                powerShellCommand.AddParameters(namedParameters);
            }
                 
            if (positionalParameters != null) {
                powerShellCommand.AddParameters(positionalParameters);
            }
 
            try {
                foreach (string str in PowerShell.Create().AddCommand("Out-String").Invoke<string>(powerShellCommand.Invoke())) {
                    Console.WriteLine(str.Trim(System.Environment.NewLine.ToCharArray()));
                }
            } catch (Exception ex){
                $(if (-not $WindowsApplication) { 'Console.WriteLine(ex.Message);' })
                $(if ($WindowsApplication) { 'MessageBox.Show(ex.Message);' })
            }
         
            powerShellCommand.Dispose();
            rs.Close();
            rs.Dispose();
    }
}
 
"@
        
        } else {
        

        if ($Embed) {
            $code += @"
        ScriptBlock embeddedFunctions = ScriptBlock.Create(@"
$embeddedCommandsText
");
        powerShellCommand = PowerShell.Create()
               .AddCommand("New-Module", false)
               .AddArgument(embeddedFunctions);
        powerShellCommand.Runspace = rs;
        try {
            powerShellCommand.Invoke();
            powerShellCommand.Dispose();
        } catch (Exception ex) {
            $(if (-not $WindowsApplication) { 'Console.WriteLine(ex.Message);' })
            $(if ($WindowsApplication) { 'MessageBox.Show(ex.Message);' })
        }
"@

        } else {
            # If the command is in a module, we'll want to go ahead and import
            # it. Let's not assume it's globally available, and import by absolute path
            if ($realCommand.Module) {
                # Unfortunately, real module path is not always the module path in powershell
                $realModulePath = $realCommand.Module.Path            
                $mayberealPath = Join-Path (Split-Path $realModulePath) "$($realCommand.Module.Name).psd1"
                if ((Test-Path $mayberealPath))  {
                     $realModulePath  = $mayberealPath 
                }
                $code += @"
        powerShellCommand = PowerShell.Create()
               .AddCommand("Import-Module", false)
               .AddArgument("$($mayberealPath.Replace('\','\\'))");
        powerShellCommand.Runspace = rs;
        try {
            powerShellCommand.Invoke();
            powerShellCommand.Dispose();
        } catch (Exception ex) {
            $(if (-not $WindowsApplication) { 'Console.WriteLine(ex.Message);' })
            $(if ($WindowsApplication) { 'MessageBox.Show(ex.Message);' })
        }
"@


            }
    
            $sma = 'System.Management.Automation, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, ProcessorArchitecture=MSIL'    
            if ($realCommand.PSSnapin -and 
                $realCommand.PSSnapin.AssemblyName -ne $sma) {
                $code += @"
        powerShellCommand = PowerShell.Create()
               .AddCommand("Add-PSSnapin", false)
               .AddArgument("$($realCommand.PSSnapin.Name)");
        powerShellCommand.Runspace = rs;
        try {
            powerShellCommand.Invoke();
            powerShellCommand.Dispose();
        } catch (Exception ex) {
            Console.WriteLine(ex.Message);
        }
"@
        
            }
        }
            $code += @"
        if (namedParameters.Contains("?")) {
            powerShellCommand = PowerShell.Create()
               .AddCommand("Get-Help")
               .AddArgument("$commandName");
            powerShellCommand.Runspace = rs;
 
        } else {
            powerShellCommand = PowerShell.Create()
                .AddCommand("$commandName");
            powerShellCommand.Runspace = rs;
 
            if (namedParameters != null) {
                powerShellCommand.AddParameters(namedParameters);
            }
                 
            if (positionalParameters != null) {
                powerShellCommand.AddParameters(positionalParameters);
            }
        }
         
        try {
            foreach (string str in PowerShell.Create().AddCommand("Out-String").Invoke<string>(powerShellCommand.Invoke())) {
                Console.WriteLine(str.Trim(System.Environment.NewLine.ToCharArray()));
            }
        } catch (Exception ex){
            $(if (-not $WindowsApplication) { 'Console.WriteLine(ex.Message);' })
            $(if ($WindowsApplication) { 'MessageBox.Show(ex.Message);' })
        }
         
        powerShellCommand.Dispose();
        rs.Close();
        rs.Dispose();
    }
}
"@

        }
        if ($OutputCSharp) { 
            $code
        } elseif ($OutputScript) {
            "" + {
param($outputPath)

} + "
`$windowsApplication = $(if ($windowsApplication) { '$true' } else { '$false' })
`$commandName = '$commandName'
`$applicationCode = @`"
$code
`"@
            "
 + {
            
if (-not $outputPath) { $outputPath = ".\$commandName.exe" } 

# resolve the output path
$unresolvedPath = $executionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath("$outputPath")
if ($unresolvedPath -notlike '*.exe') {
    Write-Warning '$unresolvedPath is not an .exe'            
}
$outputPath = $unresolvedPath 
} + "
`$addTypeParameters = @{
    TypeDefinition=`$applicationCode
    Language='CSharpVersion3'
    OutputType='$(if ($windowsApplication) { 'WindowsApplication' } else { 'ConsoleApplication' })'
    Outputassembly=`$outputPath
}
 
 
            "
 + {
if ($PSVersionTable.PSVersion -ge '3.0') {
    $null = $addTypeParameters.Remove("Language")
}
Write-Verbose "
Application Code:
 
$applicationCode
"

            
if ($windowsApplication) {
    $addTypeParameters.ReferencedAssemblies = "PresentationFramework","PresentationCore","WindowsBase"
}        
Add-Type @addTypeParameters 
if (Test-Path $outputPath) {
    $pdbPath = $outputPath.Replace('.exe','.pdb')
    if (-not $KeepDebugInformation) {
        Remove-Item -LiteralPath $pdbPath -ErrorAction SilentlyContinue
    }            
}

Get-Item -LiteralPath $outputPath -ErrorAction SilentlyContinue
            }
        } else { 
            
            # Get the output type
            if ($windowsApplication) {
                $outputType = "windowsApplication"
            } else {
                $outputType = "consoleapplication"   
            }
            
            
            if ($commandName) {
                if (-not $outputPath) { $outputPath = ".\$commandName.exe" } 
            } else {
                if (-not $outputPath) { $outputPath = ".\aScriptBlock.exe" } 
            }
            
            
            # resolve the output path
            $unresolvedPath = $psCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath("$outputPath")
            if ($unresolvedPath -notlike '*.exe') {
                Write-Warning '$unresolvedPath is not an .exe'            
            }
            $outputPath = $unresolvedPath 
        
            
            $addTypeParameters = @{
                TypeDefinition=$code
                Language='CSharpVersion3'
                OutputType=$outputType
                Outputassembly=$outputPath
            }

            if ($PSVersionTable.PSVersion -ge '3.0') {
                $null = $addTypeParameters.Remove("Language")
            }
            
            Write-Verbose "
    Application Code:
 
    $Code
    "

            
            if ($windowsApplication) {
                $addTypeParameters.ReferencedAssemblies = "PresentationFramework","PresentationCore","WindowsBase"
            }        
            Add-Type @addTypeParameters 
            if (Test-Path $outputPath) {
                $pdbPath = $outputPath.Replace('.exe','.pdb')
                if (-not $KeepDebugInformation) {
                    Remove-Item -LiteralPath $pdbPath -ErrorAction SilentlyContinue
                }            
            }
            
            Get-Item -LiteralPath $outputPath -ErrorAction SilentlyContinue
        }
    
    }
}