Reputation: 3483
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
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