从基于 C# 的 PSCmdLet 调用 CmdLet,提供输入并捕获输出

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

我正在构建一个用于出色打印的 Powershell CmdLet。它应该像 Out-Print 一样工作,但具有 winprint.

的所有花里胡哨的功能
PS> get-help out-winprint
NAME
    Out-WinPrint

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


ALIASES
    wp

为此,我需要获取我的 PSCmdLet 实现的输入流 (InputObject) 并将其传递给 Out-String,以便全部扩展和格式化。我认为最好的方法是使用 CommandInvocationIntrinsics.InvokeScript 调用 out-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);
     ...

假设我这样调用我的 cmdlet:

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

如果我理解这是如何工作的,上面的 var text 应该是 string 并且 WriteObject 调用应该显示 out-string 会显示的内容(即get-help out-winprint -full).

的结果

然而,实际上 textstring[] = { "" }(一个包含一个元素的字符串数组,一个空字符串)。

我做错了什么?

你做错了两件非常小的事:

这个方法叫做 InvokeScript 所以你传递的实际上是一个 scriptblock.

现在,你的ScriptBlock基本上是这样的:

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

Out-String

所以你可以告诉脚本中的参数,你只是没有使用它们。所以你想要更像这样的东西作为你的脚本:

Out-String -InputObject $args

只是现在的问题是 Out-String 实际上并不喜欢将 Object[] 作为 -InputObject,因此您的脚本必须类似于:

$args | Out-String

或者类似使用 foreach 的一些变体,你明白了。

您的第二个错误是您将 _psObjects 传递给了错误的参数 - 它应该是:

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

official documentation 在这方面真的很糟糕,我完全不知道另一个参数是干什么用的。

其中一个重载是列表:

input = 命令的可选输入

args = 要传递给 脚本块的参数

但是在下一次重载时它说了以下内容:

input = 用作 脚本.

输入的对象列表

args = 命令的参数数组

我只能告诉你,在我的测试中,当我按照说明进行操作时,它会起作用。希望对您有所帮助!

供参考,经过测试和工作 PS 代码:

function Test
{
    [CmdletBinding()]
    param()

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

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

Test

编辑

我应该补充一点,根据我的测试,如果您同时向 inputargs 传递某些内容,那么 $args 将在脚本中为空。就像我说的,根本不知道 input 做了什么,只是将 null 传递给它。

编辑 2

如 tig 所述,在 PowerShell issue 12137 上,传递给 input 的任何内容都将绑定到脚本块内的变量 $input,这意味着 inputargs可以用

话虽这么说...小心使用 $input - 它是一个集合,包含的内容比通过 input 参数传递的内容多:根据我的测试,索引 0 将包含boolInvokeScript() 的第二个参数传递的任何内容,索引 1 将包含一个 PipelineResultTypes 是第三个参数传递给 InvokeScript() 的任何内容。

此外,我不建议在这种情况下使用 PowerShell.Create():当您有一个 PSCmdlet 意味着您已经有一个时,为什么要创建一个新的 PowerShell 实例?

我仍然认为使用 args/$args 是最好的解决方案。当然你也可以通过使用像这样的 ScriptBlock 使事情变得更好(尽管在这种情况下完全不需要):

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

Out-String -InputObject $objects

这也会更快,因为您不再依赖(缓慢的)管道。

请不要忘记,现在您需要将 _psObjects 包裹在 object[] 中,例如:

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