Saturday, October 1, 2022
HomePowershellInternet hosting PowerShell in a Python script

Internet hosting PowerShell in a Python script


Sure Virginia, languages apart from PowerShell do exist.

I used to be working with a companion group right here at Microsoft and so they defined that they needed to parse PowerShell scripts from Python.
Their pure strategy was to invoke the PowerShell executable and assemble a command-line that did what they wanted.
I believed there could be a greater approach as creating a brand new PowerShell course of every time is pricey, so I began doing a little bit of analysis to see one thing might be achieved.
I’ve been conscious of IronPython (Python that tightly integrates .NET) for a very long time, and
we met with Jim Hugunin shortly after he arrived at Microsoft and PowerShell was simply getting underway,
however the group is utilizing cPython so I went trying to find Python modules that host .NET and located the pythonnet module.

The pythonnet bundle provides Python builders extraordinarily quick access to the dotnet runtime from Python.
I believed this bundle could be the important thing for accessing PowerShell,
after some investigation I discovered that it has precisely what I wanted to host PowerShell in a Python script.

The center

I wanted to determine a technique to load the PowerShell engine.
First, there are a few necessities to make this all work.
Dotnet must be accessible, as does PowerShell and pythonnet offers a technique to specify the place to search for dotnet.
Setting the surroundings variable DOTNET_ROOT to the set up location,
allows pythonnet a approach discover the assemblies and different help recordsdata to host .NET.

import os
os.environ["DOTNET_ROOT"] = "/root/.dotnet"

Now that we all know the place dotnet is, we have to load up the CLR and arrange the runtime configuration.
The runtime configuration describes numerous features of how we’ll run.
We will create a very easy pspython.runtimeconfig.json

{
  "runtimeOptions": {
    "tfm": "net6.0",
    "framework": {
      "title": "Microsoft.NETCore.App",
      "model": "6.0.0"
    }
  }
}

The mix of the DOTNET_ROOT and the runtime configuration allows
loading the CLR with the get_coreclr and set_runtime features.

# load up the clr
from clr_loader import get_coreclr
from pythonnet import set_runtime
rt = get_coreclr("/root/pspython.runtimeconfig.json")
set_runtime(rt)

Now that we have now the CLR loaded, we have to load the PowerShell engine.
This was slightly non-obvious.
Initially, I simply tried to load System.Administration.Automation.dll however that failed
on account of a robust title validation error.
Nonetheless, If I loaded Microsoft.Administration.Infrastructure.dll first, I can keep away from that error.
I’m not but certain about why I have to load this meeting first, that’s nonetheless one thing
I want to find out.

import clr
import sys
import System
from System import Setting
from System import Reflection

psHome = r'/decide/microsoft/powershell/7/'

mmi = psHome + r'Microsoft.Administration.Infrastructure.dll'
clr.AddReference(mmi)
from Microsoft.Administration.Infrastructure import *

full_filename = psHome + r'System.Administration.Automation.dll'
clr.AddReference(full_filename)
from System.Administration.Automation import *
from System.Administration.Automation.Language import Parser

Finally I wish to make the areas of dotnet and PSHOME configurable,
however for the second, I’ve what I want.

Now that the PowerShell engine is out there to me,
I created a few helper features to make dealing with the outcomes simpler from Python.
I additionally created a PowerShell object (PowerShell.Create()) that I’ll use in a few of my features.

ps = PowerShell.Create()
def PsRunScript(script):
    ps.Instructions.Clear()
    ps.Instructions.AddScript(script)
    consequence = ps.Invoke()
    rlist = []
    for r in consequence:
        rlist.append(r)
    return rlist

class ParseResult:
    def __init__(self, scriptDefinition, tupleResult):
        self.ScriptDefinition = scriptDefinition
        self.Ast = tupleResult[0]
        self.Tokens = tupleResult[1]
        self.Errors = tupleResult[2]

    def PrintAst(self):
        print(self.ast.Extent.Textual content)

    def PrintErrors(self):
        for e in self.Errors:
            print(str(e))

    def PrintTokens(self):
        for t in self.Tokens:
            print(str(t))

    def FindAst(self, astname):
        Func = getattr(System, "Func`2")
        func = Func[System.Management.Automation.Language.Ast, bool](lambda a : sort(a).__name__ == astname)
        asts = self.Ast.FindAll(func, True)
        return asts

def ParseScript(scriptDefinition):
    token = None
    error = None
    # this returns a tuple of ast, tokens, and errors slightly than the c# out parameter
    ast = Parser.ParseInput(scriptDefinition, token, error)
    # ParseResult will bundle the three components into one thing extra simply consumed.
    pr = ParseResult(scriptDefinition, ast)
    return pr

def ParseFile(filePath):
    token = None
    error = None
    # this returns a tuple of ast, tokens, and errors slightly than the c# out parameter
    ast = Parser.ParseFile(filePath, token, error)
    # ParseResult will bundle the three components into one thing extra simply consumed.
    pr = ParseResult(filePath, ast)
    return pr

def PrintResults(consequence):
    for r in consequence:
        print(r)

I actually needed to imitate the PowerShell AST strategies with some extra pleasant Python features.
To create the FindAst() perform, I wanted to mix the delegate in c# with the lambda characteristic in Python.
Usually, in PowerShell, this may appear like:

$ast.FindAll({$args[0] -is [System.Management.Automation.Language.CommandAst]}, $true)

However I believed from a Python script, it will simpler to make use of the title of the sort.
You continue to have to know the title of the sort,
however bing is nice for that type of factor.
As I stated, I don’t actually know the Python language,
so I count on there are higher methods to deal with the Assortment[PSObject] that Invoke() returns.
I discovered that I needed to iterate over the consequence it doesn’t matter what, so I constructed it into the comfort perform.
Anybody with strategies is greater than welcome to enhance this.

The glory

Now that we have now the bottom module collectively, we will write some fairly easy Python to
execute our PowerShell scripts.
Invoking a PowerShell script is now as simple as:

#!/usr/bin/python3

from pspython import *

scriptDefinition = 'Get-ChildItem'
print(f"Run the script: '{scriptDefinition}")
consequence = PsRunScript(scriptDefinition)
PrintResults(consequence)
/root/__pycache__
/root/dotnet-install.sh
/root/get-pip.py
/root/grr.py
/root/hosted.runtimeconfig.json
/root/pspar.py
/root/pspython.py
/root/psrun.py

You’ll discover that the output isn’t formatted by PowerShell.
It’s because Python is simply taking the .NET objects and (basically) calling ToString() on them.

It’s additionally attainable to retrieve objects and then handle formatting through PowerShell.
This instance retrieves objects through Get-ChildItem,
selects these recordsdata that begin with “ps” in Python,
after which creates a string end in desk format.

scriptDefinition = 'Get-ChildItem'
consequence = checklist(filter(lambda r: r.BaseObject.Identify.startswith('ps'), PsRunScript(scriptDefinition)))
ps.Instructions.Clear()
ps.Instructions.AddCommand("Out-String").AddParameter("Stream", True).AddParameter("InputObject", consequence)
strResult = ps.Invoke()
# print outcomes
PrintResults(strResult)
    Listing: /root

UnixMode   Consumer             Group                 LastWriteTime           Measurement Identify
--------   ----             -----                 -------------           ---- ----
-rwxr-xr-x root             dialout             6/17/2022 01:30           1117 pspar.py
-rwxr-xr-x root             dialout             6/16/2022 18:55           2474 pspython.py
-rwxr-xr-x root             dialout             6/16/2022 21:43            684 psrun.py

However that’s not all

We will additionally name static strategies on PowerShell varieties.
These of you that observed in my module there are a few language associated features.
The ParseScript and ParseFile features enable us to name the PowerShell language parser
enabling some very attention-grabbing situations.

Think about I needed to find out what instructions a script is asking.
The PowerShell AST makes {that a} breeze, however first we have now to make use of the parser.
In PowerShell, that may be achieved like this:

$tokens = $errors = $null
$AST = [System.Management.Automation.Language.Parser]::ParseFile("myscript.ps1", [ref]$tokens, [ref]$errors)

The ensuing AST is saved in $AST, the tokens in $tokens, and the errors in $errors.
With this Python module, I encapsulate that into the Python perform ParseFile,
which returns an object containing all three of these ends in a single component.
I additionally created a few helper features to print the tokens and errors extra simply.
Moreover, I created a perform that enables me to search for any sort of AST (or sub AST)
in any arbitrary AST.

parseResult = ParseFile(scriptFile)
commandAst = parseResult.FindAst("CommandAst")
instructions = set()
for c in commandAst:
    commandName = c.GetCommandName()
    # typically CommandName is null, do not embody these
    if commandName != None:
       instructions.add(c.GetCommandName().decrease())
PrintResults(sorted(instructions))

Notice that there’s a verify for commandName not being null.
It’s because when & $commandName is used, the command title can’t be
decided through static evaluation for the reason that command title is set at run-time.

…a couple of, uh, provisos, uh, a few quid professional quo

First, you need to have dotnet put in (through the install-dotnet),
in addition to a full set up of PowerShell.
pythonnet doesn’t run on all variations of Python,
I’ve examined it solely on Python 3.8 and Python 3.9 on Ubuntu20.04.
As of the time I wrote this, I couldn’t get it to run on Python 3.10.
There’s extra data on pythonnet on the pythonnet web-site.
Additionally, it is a hosted occasion of PowerShell.
Some issues, like progress, and verbose, and errors could act a bit otherwise than you
would see from pwsh.exe.
Over time, I’ll most likely add further helper features to retrieve extra runtime data
from the engine occasion.
If you need to pitch in, I’m glad to take Pull Requests or to easily perceive your use circumstances integrating PowerShell and Python.

Take it out for a spin

I’ve wrapped all of this up and added a Dockerfile (operating on Ubuntu 20.04) on
github.
To create the docker picture, simply run
Docker construct --tag pspython:demo .
from the foundation of the repository.

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments