PowerShell 3.0からはじめるTakeWhile

前回、繰り返し構文とbreak文によるtakeを考えたけど、これには欠点がある。

具体的には、breakで大域脱出可能な繰り返し構文が上流にない場合、この戦略は破綻してしまう。

PS> $x = 1..10 | take 5
PS> $x
(何も返らない)

この有様だ。ラッパーを噛ませないと使えないなんて汎用的じゃない。

Select-Object -First の謎

Select-Object には -First というオプションが存在し、これはコマンドレットの中で唯一無限リストを打ち切ることが出来る。はて、ネイティブコマンドがどうやって打ち切っているのだろうか?

灯台もと暗し。答えはまさかの Microsoft Connect にあった。

In PSv3, the pipeline can be stopped. Select-Object supports this with its -First parameter by raising a (non-public) StopUpstreamCommandsException exception.

It is of great value to be able to stop the pipeline if your mission is completed before the emitting cmdlet has provided all results.

Microsoft Connect is Retired - Collaborate | Microsoft Docs

non-public exception の StopUpstreamCommandsException を raise することで列挙完了前にパイプラインを止めることが出来る、と質問者は主張している。名前からして break で使われている FlowControlException 派生であることは容易に想像出来るだろう。

Take-While の実装

さて、パイプラインを打ち切れる素敵な non-public exception をどうやって外部から raise するのか? 答えは簡単、System.Reflectionだ。

using System;
using System.ComponentModel;
using System.Collections.ObjectModel;
using System.Management.Automation;
using System.Reflection;

namespace Anon193.PowerShell.Toys
{
	[RunInstaller(true)]
	public class ToysSnapIn : PSSnapIn
	{
		public override string Name
		{
			get
			{
				return "Anon193.PowerShell.Toys";
			}
		}

		public override string Vendor
		{
			get
			{
				return "anon_193";
			}
		}

		public override string Description
		{
			get
			{
				return "N/A";
			}
		}

		public ToysSnapIn() : base()
		{
		}
	}
}

namespace Anon193.PowerShell.Toys.Commands
{
	[Cmdlet(VerbsCommon.Select, "While")]
	public class SelectWhileCommand : Cmdlet
	{
		private SessionState predicateSessionState;
		private ConstructorInfo stopExceptionCtor;

		[Parameter(Mandatory = true, Position = 1)]
		public ScriptBlock Predicate
		{
			get; set;
		}

		[Parameter(Mandatory = true, ValueFromPipeline = true)]
		public PSObject InputObject
		{
			get; set;
		}

		public SelectWhileCommand() : base()
		{
		}

		protected override void BeginProcessing()
		{
			var sessionStateProperty = typeof(ScriptBlock).GetProperty("SessionState", BindingFlags.NonPublic | BindingFlags.Instance);
			predicateSessionState = (SessionState)sessionStateProperty.GetValue(Predicate, null);

			var asm = Assembly.Load("System.Management.Automation");
			var stopExceptionType = asm.GetType("System.Management.Automation.StopUpstreamCommandsException");
			stopExceptionCtor = stopExceptionType.GetConstructors()[0];

			base.BeginProcessing();
		}

		protected override void ProcessRecord()
		{
			var oldDollarUnderbar = predicateSessionState.PSVariable.GetValue("_");
			var oldInput = predicateSessionState.PSVariable.GetValue("input");
			predicateSessionState.PSVariable.Set("_", InputObject);
			predicateSessionState.PSVariable.Set("input", InputObject);
			try
			{
				var results = predicateSessionState.InvokeCommand.InvokeScript(predicateSessionState, this.Predicate, null);
				if (results.Count > 0)
				{
					PSObject resultHead = results[0];
					if (resultHead.ImmediateBaseObject is bool && resultHead.Equals(false))
					{
						throw (Exception)stopExceptionCtor.Invoke(new object[]{ this });
					}
				}
			}
			catch (Exception e)
			{
				throw e;
			}
			finally
			{
				predicateSessionState.PSVariable.Set("_", oldDollarUnderbar);
				predicateSessionState.PSVariable.Set("input", oldInput);
			}
			this.WriteObject(InputObject);
		}
	}
}

これを library としてコンパイルし ( System.Manmagement.Automation の参照を忘れないこと! ) 、次のような psd1 ファイルをしつらえる。

@{
# コンパイル出力先に応じて変える
RootModule = 'Anon193Toys.dll'
ModuleVersion = '1.0'
# 以下お好みで
PowerShellVersion = '3.0'
GUID = '384c96a0-4ece-490b-8f0b-45e69258059c'
}

出来上がった dll ファイルと psd1 ファイルを PowerShell ホームディレクトリ ($PSHOME) の Modules サブディレクトリにサブサブディレクトリを mkdir して配置し、Import-Module すれば、Select-While と名付けられた TakeWhile が使えるようになる。

PS > Import-Module Anon193Toys
PS > function f(){ $x = 0; while ($true) {[System.Math]::Sin([System.Math]::PI * $x++ / 100);}}
PS > $r = &f | Select-While { $_ -lt 1 }

PS > $r
0
0.0314107590781283
0.0627905195293134
0.0941083133185143
0.125333233564304
(snip)
0.982287250728689
0.987688340595138
0.992114701314478
0.99556196460308
0.998026728428272
0.999506560365732
PS >

これは Enumerator でも 繰り返し構文でも上流を強制決済出来るので、煩わしいラッパーを使う必要がない。

唯一かつもっとも重い問題は、内部 Exception を Reflection で無理矢理引き出して Raise していることだ。今後の安定性は保証出来ない。利用は研究目的にとどめたいと思う。