So, I’ve written some Powershell cmdlet’s and occasionally, might want to either use them or use Powershell built-in cmdlet’s in one of my applications. Let’s look at making this work.
Hosting Powershell
To host Powershell we include the System.Management.Automation reference (locally or from Nuget) and we can simply use
using(var ps = Powershell.Create()) { // add command(s) and invoke them }
to create a Powershell host.
Calling built-in commands/cmdlet’s
As you can see, creating the Powershell host was easy enough, but now we want to invoke a command. We can write something like
ps.AddCommand("Get-Process"); foreach (var r in ps.Invoke()) { Console.WriteLine(r); }
Invoke will return a collection of PSObjects, from each of these objects we can get member info, properties etc. but also the actual object returned from GetProcess, in this case a Process object. From this we can do the following (if required)
foreach (var r in ps.Invoke()) { var process = (Process) r.BaseObject; Console.WriteLine(process.ProcessName); }
Passing arguments into a command
When adding a command, we don’t include arguments within the command string, i.e. ps.AddCommand(“Import-Module MyModule.dll”) is wrong. Instead we pass the arguments using the AddArgument method or we supply key/value pair arguments/parameters using AddParameter, so for example
ps.AddCommand("Import-Module") .AddArgument("HelloModule.dll"); // or ps.AddCommand("Import-Module") .AddParameter("Name", "HelloModule.dll");
So the parameter is obviously the switch name without the hyphen/switch prefix and the second value is the value for the switch.
Importing and using our own Cmdlet’s
So here’s a simply Cmdlet in MyModule.dll
[Cmdlet(VerbsCommon.Get, "Hello")] public class GetHello : Cmdlet { protected override void ProcessRecord() { WriteObject("Hello World"); } }
My assumption was (incorrectly) that running something like the code below, would import my module then run the Cmdlet Get-Hello
ps.AddCommand("Import-Module") .AddArgument("HelloModule.dll"); ps.Invoke(); ps.AddCommand("Get-Hello"); foreach (var r in ps.Invoke()) { Console.WriteLine(r); }
in fact ProcessRecord for our Cmdlet doesn’t appear to get called (although BeginProcessing does get called) and therefore r is not going to contain any result even though it would appear everything worked (i.e. no exceptions).
What seems to happen is that the Invoke method doesn’t (as such) clear/reset the command pipeline and instead we need to run the code Commands.Clear(), as below
ps.AddCommand("Import-Module") .AddArgument("HelloModule.dll"); ps.Invoke(); ps.Commands.Clear(); // the rest of the code
An alternative to the above, if one is simply executing multiple commands which have no reliance on new modules or a shared instance of Powershell, might be to create a Poweshell object, add a command and invoke it and then create another Powershell instance and run a command and so on.
With a Powershell 3 compatible System.Management.Automation we can use the following. AddStatement method
ps.AddCommand("Import-Module") .AddArgument("HelloModule.dll"); .AddStatement() .AddCommand("Get-Hello"); foreach (var r in ps.Invoke()) { Console.WriteLine(r); }
Importing modules into a runspace
To share the importing of modules among Powershell host instances, we could, instead look to create a runspace (which is basically an environment space if you like) and import the module into the runspace, doing something like this
var initial = InitialSessionState.CreateDefault(); initial.ImportPSModule(new [] { "HelloModule.dll" }); var runSpace = RunspaceFactory.CreateRunspace(initial); runSpace.Open(); using(var ps = PowerShell.Create()) { ps.Runspace = runSpace; ps.AddCommand("Get-Hello"); foreach (var r in ps.Invoke()) { Console.WriteLine(r); } }
In the above code, we import our modules into the initial session, from this we create our runspace and then we associated that with the our Powershell host(s) and reuse as required.
References