PowerShell入門者ワイ、見事にアツい洗礼を受けて死亡

【初心者なりの結論】 stderr の扱いにクセがあるんですねぇこの子…

以下「PowerShell で stderr で受け取ったものを 2>&1 して stdin に回すと……火傷するぜ…!」という話がダラダラ書いてあります(ネタバレ


…というわけで、PowerShell を弄くってみたんですよ。 cmd.exe じゃ正直ちょっと力不足…… 高度な事を Windows 標準で行うには PowerShell しか無いじゃないと。 業務で「とてもセキュアです!」と謳う何も入れさせてくれない クソな 環境でとても役立つだろうと見込んでます

あと私の中では .NET用 のインタプリタという認識なので(ぉ)凄く頑張ればおそらく何でも出来そうなヨカン。 たぶんしゅごい

【お題】ffmpeg の出力をニャンニャンしたい

いきなり PowerShell で cmd.exe 向けのコマンド叩くんかいという話はさておき(ぉ

ffmpeg に動画を食わせて、そこから Audio の種別を取得したかったんです

cmd> ffmpeg -i test.webm
ffmpeg version N-83507-g8fa18e0 Copyright (c) 2000-2017 the FFmpeg developers
  built with gcc 5.4.0 (GCC)
(~~ 略 ~~)
Input #0, matroska,webm, from '.\test,webm':
  Metadata:
    encoder         : Lavf57.66.102
  Duration: 00:03:57.56, start: -0.007000, bitrate: 1132 kb/s
    Stream #0:0(eng): Video: vp9 (Profile 0), yuv420p(tv, bt709/unknown/unknown), 1280x720, SAR 1:1 DAR 16:9, 29.97 fps, 29.97 tbr, 1k tbn, 1k tbc (default)
    Stream #0:1(eng): Audio: opus, 48000 Hz, stereo, fltp (default)
At least one output file must be specified

から "opus" という文字列を取得したいなぁと。

cmd でやるとこんな感じでしょうか。 ffmpeg -i は stderr に出るので stdout に bind して pipe しました:

cmd> ffmpeg -i test.webm 2>&1 | grep "Audio: " | perl -pe "s/.+Audio: (\w+), .+/$1/g"
opus

うむ。

この結果を環境変数に入れて更に ニャンヤン しよう…となると、まぁちょっと面倒。 変数に代入したいだけなのに for で キャッキャクフフ しないと代入できないのはいささか面倒。というか本来の目的じゃないやんけという

bash なら backquote で括ってあげれば変数に入る。超楽

var=`ffmpeg -i test.webm 2>&1 | grep "Audio: " | perl -pe "s/.+Audio: (\w+), .+/$1/g`
# ※試してないです(酷

そして PowerShell も '=' で変数に代入できる。超楽!

# ps1 ファイルに書き込んで実行するとする
$result=ffmpeg -i test.webm 2>&1 | grep "Audio: " | perl -pe "s/.+Audio: (\w+), .+/`$1/g"
echo $result

echo "`n--- debug ---"
echo ('length=' + $result.Length)
echo $result[0]
echo $result[1]

結果:

+ $result=ffmpeg -i $in 2>&1 | grep "Audio: "  | perl -pe "s/.+Audio: ( ...
opus

--- debug ---
length=2
+ $result=ffmpeg -i $in 2>&1 | grep "Audio: "  | perl -pe "s/.+Audio: ( ...
opus

…え、なにこれ? なんか… 2行…なんです???

ps1 スクリプトから実行すると、叩いたコマンド自体を echo するんか…? とか思うも、結論としては違いました

PowerShell は stderr を NativeCommandError とやらに包むそうな

NativeCommandError とは?

正直よく分らないんですが!(ぉ

stderr なので Error的なオブジェクトに wrap して stream を管理する様子。 結果、元のプログラムの出力にエラーがあったコマンドラインを追加で格納して pipe に流すようです。 なにそれ

よって、単純に ffmpeg の出力を得て吐くだけにすると…

$result=ffmpeg -i $in 2>&1
echo $result

出力:

ffmpeg : ffmpeg version N-83507-g8fa18e0 Copyright (c) 2000-2017 the FFmpeg developers
発生場所 c:\path\to\flonyard\extmp3.ps1:13 文字:9
+ $result=ffmpeg -i $in 2>&1
+         ~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (ffmpeg version ...mpeg developers:String)
   [], RemoteException
    + FullyQualifiedErrorId : NativeCommandError

  built with gcc 5.4.0 (GCC)
(~~ 略 ~~)
Input #0, matroska,webm, from '.\test.webm':
  Metadata:
    encoder         : Lavf57.66.102
  Duration: 00:03:57.56, start: -0.007000, bitrate: 1132 kb/s
    Stream #0:0(eng): Video: vp9 (Profile 0), yuv420p(tv, bt709/unknown/unknown), 1280x7
20, SAR 1:1 DAR 16:9, 29.97 fps, 29.97 tbr, 1k tbn, 1k tbc (default)
    Stream #0:1(eng): Audio: opus, 48000 Hz, stereo, fltp (default)
At least one output file must be specified

頭の方になんか変なエラー情報付いてる… ('A`)

…というわけで、PowerShell さんは stderr を勝手に加工するようです。

結果、今回はそのエラー情報の部分に grep がマッチしてしまい予期せぬ結果が得られた形となりました。 嘘だと言ってよバーニィ

PowerShell の文化の中では PowerShell で統一しましょう

そもそも grep とか perl で抽出とか、cmd.exe 用のヤツ叩いてるからおかしいんじゃないの? と、思わなくも無く(ぉ

本来 cmd.exe から叩くようなモノに対して PowerShell から stdin に流し込もうとした場合、pipe に流れる object を toString() したようなものを流す…んだと思ってます(未調査

んじゃぁ、grep とか extract の部分も面倒くさがらずに PowerShell にしたら、pipe に流れる object を PowerShell界の流儀に従って良い感じに扱ってくれるんじゃなかろうかと思い、頑張って PowerShell に置き換えてみました:

$result = ffmpeg -i $in 2>&1 | Select-String "Audio: "
$result = $result -replace '.+Audio: (\w+),.+','$1'
echo $result

# --- 結果 ---
opus

おっ、ステキステキー

select-string による grep で error が発生した command line が unmatch だった… という可能性もおそらく… 無さそう… たぶん…… たぶん……

結論

というわけで PowerShell での stderr を cmd.exe のノリで触ると火傷するというお話でした。

今のところ致命的にハマったのはココだけですかねぇ…

イマイチまだ文化に慣れてませんけど cmd.exe よりかは楽出来そうな気はしています。 もうちょっと文化を知って慣れたい所……

link