tig
tig

Reputation: 3483

Invoking CmdLet from a C#-based PSCmdLet, providing input and capturing output

I'm building a Powershell CmdLet for awesome printing. It should function just like Out-Print but with all the bells and whistles of winprint.

PS> get-help out-winprint
NAME
    Out-WinPrint

SYNTAX
    Out-WinPrint [[-Name] <string>] [-SheetDefintion <string>] [-ContentTypeEngine <string>]
    [-InputObject <psobject>] [<CommonParameters>]


ALIASES
    wp

For this to work, I need to take the input stream (InputObject) of my PSCmdLet implementation and pass it through Out-String so it's all expanded and formatted. I'm thinking the best way to do this is to use CommandInvocationIntrinsics.InvokeScript to invoke out-string, which should give me the output as a string...

        protected override void ProcessRecord() {
            if (InputObject == null || InputObject == AutomationNull.Value) {
                return;
            }

            IDictionary dictionary = InputObject.BaseObject as IDictionary;
            if (dictionary != null) {
                // Dictionaries should be enumerated through because the pipeline does not enumerate through them.
                foreach (DictionaryEntry entry in dictionary) {
                    ProcessObject(PSObject.AsPSObject(entry));
                }
            }
            else {
                ProcessObject(InputObject);
            }

        }

        private void ProcessObject(PSObject input) {

            object baseObject = input.BaseObject;

            // Throw a terminating error for types that are not supported.
            if (baseObject is ScriptBlock ||
                baseObject is SwitchParameter ||
                baseObject is PSReference ||
                baseObject is PSObject) {
                ErrorRecord error = new ErrorRecord(
                    new FormatException("Invalid data type for Out-WinPrint"),
                    DataNotQualifiedForWinprint,
                    ErrorCategory.InvalidType,
                    null);

                this.ThrowTerminatingError(error);
            }

            _psObjects.Add(input);
        }

        protected override async void EndProcessing() {
            base.EndProcessing();

            //Return if no objects
            if (_psObjects.Count == 0) {
                return;
            }

            var text = this.SessionState.InvokeCommand.InvokeScript(@"Out-String", true, PipelineResultTypes.None, _psObjects, null);

            // Just for testing...
            this.WriteObject(text, false);
     ...

Assume I invoked my cmdlet like this:

PS> get-help out-winprint -full | out-winprint`

If I understand how this is supposed to work, the var text above should be a string and WriteObject call should display what out-string would display (namely the result of get-help out-winprint -full).

However, in reality text is string[] = { "" } (an array of strings with one element, an empty string).

What am I doing wrong?

Upvotes: 1

Views: 479

Answers (1)

cogumel0
cogumel0

Reputation: 2661

You're doing two very small things wrong:

The method is called InvokeScript so literally what you're passing is a scriptblock.

Right now, your ScriptBlock is basically like this:

$args = @(<random stuff>) # this line is implicit of course, 
                          # and $args will have the value of whatever your _psObjects has

Out-String

So as you can tell the arguments made it to the script, you're just not using them. So you want something a bit more like this instead as your script:

Out-String -InputObject $args

Only now the problem is that Out-String doesn't actually like being given a Object[] as an -InputObject so instead your script has to be something like:

$args | Out-String

Or some variation of that like using a foreach, you get the idea.

Your second error is that you're passing _psObjects onto the wrong parameter - it should be:

this.SessionState.InvokeCommand.InvokeScript(<ScriptBlock>, true, PipelineResultTypes.None, null, _psObjects);

The official documentation is really bad on this and I have absolutely no idea what the other parameter is for.

On one of the overloads is lists:

input = Optionall input to the command

args = Arguments to pass to the scriptblock

But on the next overload it says the following:

input = The list of objects to use as input to the script.

args = The array of arguments to the command.

All I can tell you is, in my tests it works when I do it as stated. Hope that helps!

For reference, tested and working PS code:

function Test
{
    [CmdletBinding()]
    param()

    $results = $PSCmdlet.SessionState.InvokeCommand.InvokeScript('$args | Out-String', $false, "None", $null, "Hello World!")

    foreach ($item in $results)
    {
        $item
    }
}

Test

EDIT

I should add that according to my tests, if you pass something to both input and args then $args will be empty inside the script. Like I said, no actual idea what input does at all, just pass null to it.

EDIT 2

As mentioned by tig, on PowerShell issue 12137, whatever gets passed to input will be bound to the variable $input inside the scriptblock which means either input or args can be used.

That being said... be careful of using $input - it is a collection that will contain more than what is passed via the input parameter: according to my tests index 0 will contain a bool that is whatever is passed on 2nd parameter of InvokeScript() and index 1 will contain a PipelineResultTypes that is whatever was passed to InvokeScript() on the 3rd parameter.

Also I would not recommend using PowerShell.Create() in this case: why create a new instance of PowerShell when you have a PSCmdlet which implies you already have one?

I still think using args/$args is the best solution. Of course you could also make things a lot nicer (although completely unneeded in this case) by using a ScriptBlock like:

[CmdletBinding()]
param
(
    [Parameter(Mandatory)]
    [PSObject[]]
    $objects
)

Out-String -InputObject $objects

This will be faster as well since you are no longer relying on the (slow) pipeline.

Just don't forget that now you need to wrap your _psObjects around an object[] like:

this.SessionState.InvokeCommand.InvokeScript(<ScriptBlock>, true, PipelineResultTypes.None, null, new object[] {_psObjects});

Upvotes: 1

Related Questions