首页 > 解决方案 > 从基于 C# 的 PSCmdLet 调用 CmdLet,提供输入和捕获输出

问题描述

我正在构建一个用于出色打印的 Powershell CmdLet。它的功能应该与winprintOut-Print的所有花里胡哨的功能一样。

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

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


ALIASES
    wp

为此,我需要获取InputObject我的实现的输入流 ( )PSCmdLet并将其传递,Out-String因此它全部被扩展和格式化。我在想最好的方法是使用CommandInvocationIntrinsics.InvokeScriptto invoke 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`

如果我理解这应该如何工作,那么text上面的 var 应该是 astring并且WriteObjectcall 应该显示out-string将显示的内容(即 的结果get-help out-winprint -full)。

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

我究竟做错了什么?

标签: c#powershellpscmdlet

解决方案


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

从字面上看,该方法被称为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实际上并不喜欢被给予 a Object[]-InputObject所以你的脚本必须是这样的:

$args | Out-String

或者一些变体,比如使用 a foreach,你就明白了。

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

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

官方文档在这方面真的很糟糕,我完全不知道另一个参数是做什么用的。

其中一个重载是列表:

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 问题 12137上,传递给的任何内容都input将绑定到脚本块$input内的变量,这意味着要么使用input要么args可以使用。

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

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

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

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

Out-String -InputObject $objects

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

只是不要忘记,现在你需要把你_psObjectsobject[]喜欢包裹起来:

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

推荐阅读