The Man Who Fell From The Wrong Side Of The Sky:2019年5月19日分

2019/5/19(Sun)

[Windows] PowerShellで生活するために - ForEach-Objectコマンドレット編

@ 這いずり回る混乱

PowerShell初心者(ワイもやで)が混乱するポイントに、イテレーション処理にforeachステートメントとForEach-Objectコマンドレットのふたつある事が上げられる。

まずforeachステートメントなんてみた瞬間「テメーどの面下げて関数型言語を名乗ってんだ *1オルァン!」と血圧が上昇する関数型言語ラーの方々はおいといて

foreach ($elm in "item1", "item2", ...) {
	Write-Host $elm
}

とまぁこれだけみると普通の手続き型プログラミング言語でございますな。

しかしこれだとfor文禁止じゃ高階関数で後悔させてやるオルァン!と息を巻く原理主義の方々が激怒必至なので

"item1", "item2", ... | ForEach-Object { Write-Host $_ }

と書くことも可能なわけです、どうですか…心が落ち着きましたか…どうぞご着席ください。 なお今度はだいたい兼任してると思われるUNIX哲学原理主義が目覚め、パイプを流れるストリームが汎用インタフェースのテキストでなくオブジェクトであることに激怒して以下略。

ちなみに後者はこれ関数型言語Java(しろめ)で書くなら

Arrays.asList("item1", "item2", ...).forEach(System.out::println);

と同じですわなこれ、なぜJavaで書いた!(突然暴れだす患者)

@ 混乱を助長する余計なもの

なおForEach-Objectにはエイリアスが存在して紛らわしいことに

"item1", "item2", ... | foreach { Write-Host $_ }

とも書けてしまう、しかしforeachエイリアスはforeachステートメントとは明確に違うので混同してはならない(戒め) *2

さらにややこしいことにこうも書ける。

"item1", "item2" | % { Write-Host $_ }

この場合もforeachと同様に「%」はForEach-Objectコマンドレットのエイリアスなのだ。

おそらく「%」なんてエイリアスを用意したのは、シェルで使う場合にForEach-Objectは長過ぎるからだろう、他にもWhere-Objectのエイリアスである「?」などがあり

(Invoke-WebRequest 'http://www.microsoft.com/').AllElements | Where-Object { $_.tagName -eq 'h1' } | ForEach-Object { Write-Host $_.innerText }

(wget 'http://www.microsoft.com/').AllElements | ? { $_.tagName -eq 'h1' } | % { Write-Host $_.innerText }

と書き直せる、わーいタイプ量が減ったよ!って疑問符とか特攻の拓ならまだしもプログラミング言語としては検索性最悪でどんな判断だとしか言いようが無い、こういう可読性クソなところがPerlっぽくて嫌いなのに使ってしまうビクンビクン(クソ言語ソムリエ)。

なので現在では推奨されない書き方になってるはず、Set-StrictModeで禁止できたような記憶があったんだが今マニュアル確認したら無いな。 単にVisual Studio Codeが警告だしてただけかもしれない、あと使いづらいのでPowerShell ISE for PowerShell 6はやくだしてやくめでしょ。

@ 混乱による悲劇

話をもどす、foreachとForEach-Objectの混同でありがちなのが、デフォルト変数(Perl用語)である$_の扱いが違うことに気づかないサルコーダーが、気楽にStackOverflowやQiitaとかいうYahoo!知恵袋の類似サイトからコピペしてバグるパターン、ほんまインターネットとPowerShellは地獄だぜ!

foreach ($i in Get-Content 'unko.txt') {
	if ($_ -match '^\s*#') {
		...
	}
}

$_に入ってる値はその前にやった処理で実行結果は神のみぞ知るってやつ。

これPerlみたいにデフォルト変数使えるようにして

foreach (Get-Content 'unko.txt') {
	if ($_ -match '^\s*#') {
		...
	}
}

と書ければある程度は事故防げたんじゃねぇかな…まぁでもコピペコーダーを滅するのが正道か…

ちなみに$_もエイリアスで本当は$PSItemであり呼び方も自動変数というのだけど、まあPerl使いなのでこっち使って/呼ばせて貰いますわ(老害)。

あと初心者的には

foreach ($line in Get-Content 'unko.txt') {
	if ($line -match '^\s*#') {
		continue
	}
	Write-Host $line
}

みたいにある条件(この場合コメント行を無視する)の場合continueで処理をすっ飛ばすなんてコードを、違いを理解せずにForEach-Objectで書き直して

Get-Content 'unko.txt' | ForEach-Object {
	if ($_ -match '^\s*#') {
		continue
	}
	Write-Host $_
}

が期待通りに動かないとかあるあるなんやな。

これbreak/continueステートメントがfor/foreach/while/doステートメントに続くブロック内では期待通りに動作するのは当然だけど、なぜForEach-Objectに続くブロックでは動かないのか初心者はそりゃ混乱しますわね。

答えは簡単、後者はただのブロックではなくベンザブロックでもなく「スクリプトブロック」と呼ばれるもので、他の言語だとラムダ式とか無名関数を引数として渡してるのと同じことなんですわ。

なので関数ポインタのように変数にスクリプトブロックを代入することもできる。

$doit = { Write-Host $_ }
"item1", "item2", ... | ForEach-Object $doit

ただしスクリプトブロックはclosureつまり関数(は木を切るヘイ)閉包ではないので束縛変数を持たないから引数も渡せないとかがね…(また回を改めて書く)。

そんでスクリプトブロック中でbreak/continueが呼ばれると、そこでForEach-Object処理そのものを大域脱出してしまうのだ、ていうか文法エラーの方がよくない?これ。

ちなみに初心者が困り果てて世の害悪こと教えて掲示板で「助けて!動かない!」すると80%の確立でcontinueやめてreturnにすれば動くよ!というポイント稼ぎ回答が返ってくると思う。

Get-Content 'unko.txt' | ForEach-Object {
	if ($_ -match '^\s*#') {
		return
	}
	Write-Host $_
}

せやな、とりあえず期待した通りの動きにはなるかもしれんけど不正解。

正解はですね、関数型スタイルならそもそもこんなとこにifステートメント書く自体が間違いなのでfilterを使えなのだ、Where-Objectを使って

Get-Content 'unko.txt' | Where-Object { $_ -notmatch '^\s*#' } | ForEach-Object { Write-Host $_ }

と書くべきなのである。

@ 混乱こそ美

ある程度PowerShellへの理解が進むと、手続き型と関数型が混在するとソース読み辛いから関数型スタイルで統一するね…という気分になるのだけど、実はそうもいかないところがPowerShellのつらいところ。 実はForEach-Objectはとてつもなく遅い処理なのだ、ベンチとるのめんどくさいしググれば先人による記事あるからワイが書くまでもないしそっち読んで。

ちなみに最速はforeachステートメントですらなくforステートメントなので

$list = "item1", "item2", ...
for ($i = 0; $i -lt $list.length; ++$i) {
	Write-Host $list[$i]
}

というモダンさの欠片も存在しないコードが大正義だったりする、この故郷のような安心感。

しかし巨大なテキストファイルを全部配列に読み込んだりすると今度はメモリ使用量が問題になったりするわけで

スタイル 速度 メモリ使用量 可読性
for/foreach × ×
ForEach-Object ×

を考慮しつつ結局はケースバイケースなのだ。

@ ここで明かされるForEach-Objectの驚くべき正体

そして最後に衝撃的な事実を告げよう、ForEach-Objectって上の例ではわざと使わなかったんだけど

"item1", "item2", ... | ForEach-Object { Write-Host "header" } { Write-Host $_ } { Write-Host "footer" }

のようにスクリプトブロックを3つ引数として指定できるのですわ、これオプションを省略せずに書くと

"item1", "item2", ... | ForEach-Object -Begin { Write-Host "header" } -Process { Write-Host $_ } -End { Write-Host "footer" }

となる、さあだんだん前世で体験した何かを思い出しましたね?そうこれはawk(1)のBEGINとENDブロック!

つまりはだ、ForEach-Objectってーのは関数型言語だ高階関数だmapだなんて小難しい事より *3、UNIX文化におけるawk(1)の代替コマンドレットだったんだよ!そのためのパイプでなんという恐ろしいUNIXの再発明!

これコマンドレット名はForEach-ObjectではなくAho-Weinberger-Kernighan、エイリアスもforeachとか%じゃなくてawkで良かったのでは…Invoke-WebRequestにwget(1)なんてエイリアスつけるくらいあっちチラチラみてるわけだし…

なおawk(1)より億倍使いづらい、なんせマッチする行にifステートメント書いたり区切り文字での分割も自分でせんとあかん。

awk -F'\t' '!/^\s*#/ { print $2 }' unko.txt

がPowerShellだと

Get-Content 'unko.txt' | ForEach-Object  { if ($_ -notmatch '^\s*#') { Write-Host ($_ -split '`t')[1] } } }

になるってどう考えても劣化しすぎよね(もっと簡単に書ける書き方あったらスマン)、複数条件ある場合はifステートメントの代わりにWhere-Object使いづらいしGet-Contentすなわちcat(1)の代わりに-InputObject使えないから Useless Use Of Cat問題もあるわけだし。まぁパイプをオブジェクトストリームとしたのが失敗なのだ。

関数型言語の思想はUNIX哲学と似ているとはいうけど、Microsoftはメシマズアレンジが得意だったな…ワイ語れるほど関数型言語もUNIXも知らんけど。

*1:そもそも名乗ってたっけかな…自信無くなってきたぞ…でも開発名Monadだしな…
*2:混乱に拍車をかけるのがPowerShellの大文字小文字同一視な文法でもあり、MicrosoftをMicrosoftたらしめるBASICの息吹を感じる(しろめ)。
*3:勘のいい人はそもそもreduceに相当するコマンドレットがせいぜいMeasure-Objectくらいしか無さそうな時点で気づくな(しろめ)

[Hardware] BUFFALO IFC-USP SCSIカードはWindows 10 32bitならWindows 2000用のドライバで動作する

先月くらいに BUFFALO IFC-USPというAdvanSys ASC3050Bというコントローラー搭載のSCSIカードを108円で買ってたんだが記事にしてなかったな。

調べた限りではAdvansysはWindows 2000までのドライバしか出してなくて、XP/2003 ServerやVista/2008 Server向けに64bitドライバは提供してなかったようで、Windows 10なんかで使うにはWindows 2000用の32bit用の未署名ドライバを入れるしかない。 まぁ32bitならドライバ署名云々いわれることは無いので警告無視してインストールするだけでOK。

ちなみにRATOC Systemが同チップを採用しているREX PCI-30にWindows 10 64bit対応の有償ドライバを販売してるんだよな。 おそらく自分ところでわざわざスクラッチでドライバ書いたのかもね、ベンダチェックとかなければ購入すればIFC-USPも動くかもしれない。