从基于 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
).
的结果
然而,实际上 text
是 string[] = { "" }
(一个包含一个元素的字符串数组,一个空字符串)。
我做错了什么?
你做错了两件非常小的事:
这个方法叫做 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
编辑
我应该补充一点,根据我的测试,如果您同时向 input
和 args
传递某些内容,那么 $args
将在脚本中为空。就像我说的,根本不知道 input
做了什么,只是将 null
传递给它。
编辑 2
如 tig 所述,在 PowerShell issue 12137 上,传递给 input
的任何内容都将绑定到脚本块内的变量 $input
,这意味着 input
或 args
可以用
话虽这么说...小心使用 $input
- 它是一个集合,包含的内容比通过 input
参数传递的内容多:根据我的测试,索引 0 将包含bool
是 InvokeScript()
的第二个参数传递的任何内容,索引 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});
我正在构建一个用于出色打印的 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
).
然而,实际上 text
是 string[] = { "" }
(一个包含一个元素的字符串数组,一个空字符串)。
我做错了什么?
你做错了两件非常小的事:
这个方法叫做 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
编辑
我应该补充一点,根据我的测试,如果您同时向 input
和 args
传递某些内容,那么 $args
将在脚本中为空。就像我说的,根本不知道 input
做了什么,只是将 null
传递给它。
编辑 2
如 tig 所述,在 PowerShell issue 12137 上,传递给 input
的任何内容都将绑定到脚本块内的变量 $input
,这意味着 input
或 args
可以用
话虽这么说...小心使用 $input
- 它是一个集合,包含的内容比通过 input
参数传递的内容多:根据我的测试,索引 0 将包含bool
是 InvokeScript()
的第二个参数传递的任何内容,索引 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});