ConvertTo-PowerShell - Converting C# to PowerShell

PowerShell Pro Tools Open-Source PowerShell C#

April 5, 2022

quote Discuss this Article

In this post, we’ll discuss how ConvertTo-PowerShell works.

Table of Contents

Introducting the CodeConversion Module

Up until recently, ConvertTo-CSharp and ConvertTo-PowerShell were a part of PowerShell Pro Tools. We recently moved them out of PowerShell Pro Tools and into an open-source and free module called CodeConversion. You can use the cmdlets found in it to perform the exact same operations that were available previously in the paid tool.

For example, you can convert C# code to PowerShell script. This is an example of intent-based conversion.

Install-Module CodeConversion
ConvertTo-PowerShell -Code 'System.Diagnostics.Process.GetProcesses()'
# Outputs: Get-Process

You can also convert standard structures, like loops, from C# to PowerShell. Take this class, for example.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CodeConverter.Test.Languages.CSharp
{
    public class Class
    {
        public void Method()
        {
            for(int i = 0; i < 100; i++)
            {
                var t = i;
            }
        }
    }
}

Running the ConvertTo-PowerShell cmdlet against this would yield the following.

function Method
{
	for([int]$i = 0; $i -lt 100; $i++)
	{
		$t = $i
	}
}

You can view more examples and the source for this module here.

Abstract Syntax Tree Conversion

Most of the syntax-based conversions are conducted using abstract syntax trees. We run the C# code or PowerShell script through their respective parsers to yield an abstract syntax tree. An abstract syntax tree (or AST) is a tree-based representation of the code. It allows for scripted analysis of static code without actually having to run it. ASTs are useful for tools like linters (like PSScriptAnalyzer) or IntelliSense.

Most languages have common structures that are consistent across syntax trees. For example, you’ll find variables, loops, functions or methods, strings and other constants across languages. We take advantage of this by generating an intermediate AST that is generic. So for example, we take the PowerShell AST for a script, convert it to the generic version of the AST and then use a code generator to take the generic AST and write it out as a C# file.

Vice versa, we can parse a C# code file into the C# AST, convert it to a generic AST and then write out PowerShell script. Because the code generation aspect accepts a generic AST, it’s possible to convert from additional languages. Currently, CodeConversion only works between C# and PowerShell.

As a simple example of how this works, we can look at how we convert strings. Using the AstVisitor class, we can walk the syntax tree and start to generate a custom one.

For example, the below method visits a StringConstantExpressionAst from within a PowerShell script. It then generates a generic StringConstant for our generic AST type.

public override AstVisitAction VisitStringConstantExpression(StringConstantExpressionAst stringConstantExpressionAst)
{
    _currentNode = new StringConstant(stringConstantExpressionAst.Value);

    return AstVisitAction.SkipChildren;
}

On code generation end, we take the generic StringConstant and generate some C# code.

public override void VisitStringConstant(StringConstant node)
{
    var escapedString = node.Value.Replace("\\", "\\\\");

    Append("\"" + escapedString + "\"");
}

Intent Conversion

Another feature of the CodeConversion module is converting based on code intent. This means that we have some rules to evaluate what the code is attempting to do and then generate code based on that rather than merely on syntax. Often languages will not have a direct conversion so it makes more sense to convert code this way.

We use another AST Visitor to determine intent. If it finds characteristics of that intent, it will generate an Intent object in the AST rather than a syntax object.

On the PowerShell side, we provide a mapping file that includes JSON structures to define intent. For example, if we find the Get-Process cmdlet being used (where there is no matching cmdlet in C#) we generate an intent called GetProcess and provide a parameter name or ID.

  {
    "CommandName": "Get-Process",
    "Intent": "GetProcess",
    "ParameterMappings": {
      "Name": "Name",
      "Id": "Id"
    }
  },

On the C# side of things, we have a class for each type of intent. Rather than generating code based on syntax, it uses the intent to generate a completely different AST form. The Get-Process call is processed by this intent writer. It takes the aguments provided (name and ID) and then generates the proper C# code for the intent.

public class GetProcessIntent : BaseIntentWriter
{
    public override string Intent { get { return "GetProcess"; } }
    public override string Write(Dictionary<string, string> arguments)
    {
        if (arguments.ContainsKey("Id"))
        {
            return $"System.Diagnostics.Process.GetProcessById({arguments["Id"]})";
        }

        if (arguments.ContainsKey("Name"))
        {
            return $"System.Diagnostics.Process.GetProcessesByName({arguments["Name"]})";
        }

        return $"System.Diagnostics.Process.GetProcesses()";
    }
}

Conclusion

A code conversion tool like this requires directly mapping syntax from one language to another as well as the intent of any snippet of code. It can be highly accurate but it requires a lot of work to provide that accuracy. Tools such as GitHub Copilot are making headway in creating a system that will be able to adapt easier and eventually provide just as high of accuracy as a tool like this.

We are very much open to issues and pull requests on the CodeConversion repository. Feel free to extend with new syntax, intents or even new languages.