如何让编译后的Cmdlet即时输出脚本块生成的每个对象?
如何让编译后的Cmdlet在调用脚本块时即时输出每个对象?
原生Invoke-Command的即时输出表现
使用原生Invoke-Command时,脚本块输出的对象会被即时传递到下游管道:
Invoke-Command { 'a'; Write-Verbose 'a' -Verbose 'b'; Write-Verbose 'b' -Verbose } | . { process { Write-Verbose "downstream_$_" -Verbose } }
输出结果:
VERBOSE: downstream_a VERBOSE: a VERBOSE: downstream_b VERBOSE: b
可以看到,脚本块每输出一个对象,下游的process块就会立即被调用处理。
自定义Cmdlet的延迟输出问题
如果用自定义编译的Cmdlet替换Invoke-Command,默认实现会先收集脚本块的所有输出,直到脚本执行完毕才一次性输出:
Add-Type ` -PassThru ` -TypeDefinition @' using System.Management.Automation; using System.Collections.Generic; namespace n { [Cmdlet(VerbsLifecycle.Invoke,"SomeScriptBlockThing")] public class InvokeSomeScriptBlockThing : Cmdlet { [Parameter(Mandatory=true,Position=1)] public ScriptBlock ScriptBlock { get; set; } protected override void EndProcessing() { var output = ScriptBlock.Invoke("some_fancy_argument"); foreach (var o in output) { WriteObject(o); } } } } '@ | % Assembly | Import-Module Invoke-SomeScriptBlockThing { param($SomeFancyArgument) 'a'; Write-Verbose 'a' -Verbose 'b'; Write-Verbose 'b' -Verbose } | . { process { Write-Verbose "downstream_$_" -Verbose } }
输出结果:
VERBOSE: a VERBOSE: b VERBOSE: downstream_a VERBOSE: downstream_b
所有对象被累积后一次性输出,下游管道只能在脚本块执行完成后才开始处理。
问题背景
实际需求是要向脚本块传递CancellationToken,要求Cmdlet的生命周期长于该token,因此需要Cmdlet直接调用脚本块并传递参数,但当前实现的延迟输出不符合要求。旧方案中通过启动独立PowerShell实例的方式会破坏父作用域的访问权限,不可取。
解决方案:实时捕获脚本块输出
核心是不要使用ScriptBlock.Invoke()收集全部结果,而是用PSDataCollection实时捕获脚本块的输出,并在数据新增时立即调用WriteObject输出到管道。修改后的Cmdlet代码如下:
Add-Type ` -PassThru ` -TypeDefinition @' using System.Management.Automation; using System.Collections.Generic; namespace n { [Cmdlet(VerbsLifecycle.Invoke,"SomeScriptBlockThing")] public class InvokeSomeScriptBlockThing : Cmdlet { [Parameter(Mandatory=true,Position=1)] public ScriptBlock ScriptBlock { get; set; } protected override void EndProcessing() { // 创建实时输出收集集合 using var outputCollection = new PSDataCollection<object>(); // 订阅数据新增事件,即时输出对象 outputCollection.DataAdded += (sender, e) => { var collection = (PSDataCollection<object>)sender; // 处理从当前索引开始的新增数据 for (int i = e.Index; i < collection.Count; i++) { WriteObject(collection[i]); } }; // 准备要传递给脚本块的参数变量 var variables = new List<PSVariable> { new PSVariable("SomeFancyArgument", "some_fancy_argument") }; // 使用InvokeWithContext执行脚本块,将输出定向到实时集合 ScriptBlock.InvokeWithContext(null, variables, outputCollection); // 标记输出集合完成,确保所有数据都被处理 outputCollection.Complete(); } } } '@ | % Assembly | Import-Module
修改后的效果
再次运行测试代码,输出会和原生Invoke-Command一致:
VERBOSE: downstream_a VERBOSE: a VERBOSE: downstream_b VERBOSE: b
脚本块每生成一个对象,就会被即时传递到下游管道处理,同时保留了对父作用域的访问权限,也能正常传递CancellationToken等参数。
内容的提问来源于stack exchange,提问作者alx9r
相关产品推荐
相关产品推荐

