Question:
I have a PS-Module completely written in C#. It contains about 20 Cmdlets that are already in production. Some of these “share code”. Take this example:
I have a Cmdlet called InvokeCommitCommand
that produces a “changeset”. This Cmdlet also publishes metadata of this changeset. I would now like to create a new Cmdlet called PublishCommitCommand
that can be called independantly to execute the “publishing” of an already existing changeset. I would therefore like to refactor InvokeCommitCommand
to make use of the new Cmdlet PublishCommitCommand
and avoid code duplication.
More generally speaking … I am trying to invoke a cmdlet CommandB
from cmdlet CommandA
. They are defined as follows
1 2 3 4 5 6 7 8 9 10 |
public CommandA : PSCmdlet { ... } public CommandB : PSCmdlet { ... } |
I have a few options here. But none of them work.
1. Option
Invoke CommandB
by creating an instance of it. That would’ve been my first guess. Like so:
1 2 3 |
var cmd = new CommandB(); cmd.Invoke(); |
Unfortunately that does not work. I get the exception:
Cmdlets derived from PSCmdlet cannot be invoked directly …
So … next option.
2. Option
Create an instance of PowerShell and run the command. Like so:
1 2 3 4 |
var ps = PowerShell.Create(); ps.AddCommand("CommandB"); ps.Invoke(); |
Unfortunately that doesn’t work either. This causes a new PowerShell instance to be created and therefore I loose all stream redirections I may have attached to the current PowerShell instance I am running in.
I know I can reuse the runspace. But using the same runspace does NOT save me from losing my redirections. If CommandB
would call Write-Verbose "Huzzah!"
, I would not see that ‘Huzzah!’ anywhere.
In short: I need to run the CommandB
in the same PS instance as CommandA
3. Option
Use a ScriptBlock. Like so:
1 2 3 |
var sb = ScriptBlock.Create("CommandB"); sb.Invoke(); |
That’s pretty nice. But the problem here is, that I have no means to pass any complex class arguments to the script block. If CommandB
has a parameter of type … let’s say PSCredential
, I have no easy way to pass that parameter to the script. If I had a PowerShell
object, I could easily do
1 2 3 4 5 |
PowerShell ps ps.AddCommand("CommandB"); ps.AddArgument("Credential", someCredentialObject); ps.AddArgument("TargetUri", new Uri("www.google.de")); |
But I can not that with a ScriptBlock
. True, I could use InvokeWithContext
which allows me to pass variables to the scriptblock, but I would need to “wrap” each complex argument in a variable first… rather cumbersome.
Conclusion
Any ideas? The best thing would be if I somehow could – from inside CommandA
get access to the current instance of PowerShell
I am running in. I could then leverage option 2 without the issue of creating a new instance. But I do not know if that is even possible…
Answer:
The solution I came up with in the end is a helper class that implements the method suggested by PetSerAl. I use a ScriptBlock like in my 3rd option above, but with a few changes to make passing parameters less tedious.
So here is my helper class that does the job quite nicely:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
public class PsInvoker { public static PSObject[] InvokeCommand(string commandName, Hashtable parameters) { var sb = ScriptBlock.Create("param($Command, $Params) & $Command @Params"); return sb.Invoke(commandName, parameters).ToArray(); } public static PSObject[] InvokeCommand { return InvokeCommand(Extensions.GetCmdletName } public static PsInvoker Create(string cmdletName) { return new PsInvoker(cmdletName); } public static PsInvoker Create { return new PsInvoker(Extensions.GetCmdletName } private Hashtable Parameters { get; set; } public string CmdletName { get; } public bool Invoked { get; private set; } public PSObject[] Result { get; private set; } private PsInvoker(string cmdletName) { CmdletName = cmdletName; Parameters = new Hashtable(); } public void AddArgument(string name, object value) { Parameters.Add(name, value); } public void AddArgument(string name) { Parameters.Add(name, null); } public PSObject[] Invoke() { if (Invoked) throw new InvalidOperationException("This instance has already been invoked."); var sb = ScriptBlock.Create("param($Command, $Params) & $Command @Params"); Result = sb.Invoke(CmdletName, Parameters).ToArray(); Invoked = true; return Result; } } |
This class basically provides two methods of invoking a cmdlet:
- You can use its static methods
InvokeCommand
and pass the name of the cmdlet plus any parameters as aHashtable
. - Create an instance of the PsInvoker class and use
AddArgument
to add parameters and then useInvoke
to run the cmdlet.
Thanks again to PetSerAl.