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.