しばたテックブログ

気分で書いている技術ブログです。

PowerShellの"罠"と呼ばれるモノについて

私は普段ブログのネタ探しのためにいくつかのSNSやフォーラムなどを巡回しているのですが、その際に

PowerShellの罠にハマる

といった表現をそれなりに目にしてきました。

大抵のツールに初学者にとってハマりやすい"罠"はあると思いますが、PowerShellにも"罠"と言われても仕方のない部分は結構あります。
本エントリではこの"罠"について思うところを書き連ねていきます。

"罠"の一覧

PowerShellはPowerShell 1.0がリリースされてからもうすぐ12年目を迎えつつある歴史の長いツールです。
このため"罠"の数もそれなりにあり、既に有志によって一覧はまとめられています。

github.com

個々の"罠"の詳細についてはこちらのリポジトリをご覧いただければ十分でしょう。

また、日本語の情報としては、

がPowerShellを使う上でハマりやすいポイントを良く解説してくれており参考になります。

PowerShell 3.0 での破壊的変更

こちらは"罠"と少し違うのですが、PowerShellはPowerShell 3.0でランタイムが刷新され、それに合わせて幾つかの破壊的変更が生じています。
この破壊的変更の一覧はWMF 3.0のリリースノートに軽く記載されているだけで詳細な解説がありません。

情報の得にくさとしてはこれも"罠"といっても良いかもしれません。*1

【2018/12/01追記】

PowerShell 3.0での破壊的変更の一覧をブログにまとめました。

blog.shibata.tech

【追記ここまで】

"罠"を感じる心理的な問題

あくまで私の経験上ですが、PowerShellを学ぶ人が"罠"を感じるのには心理的な要因もあるのではないかと思っています。

非常に大事な前提なのですが、

PowerShellはコマンドプロンプトでは無いし*shでも無い。

この前提を誤解して"罠"と見做されるケースが多い様に感じます。
これは一見当たり前の様に見えますが非常に大切なことです。

まず、PowerShellは時代的な経緯からコマンドプロンプトの後継の様に誤解されることが多いのですが、後発ではあるものの後継ではありませんしコンセプトが異なるツールです。
また、Bash等のシェルから影響を受けている部分があるため、いくつか概念や語彙に重なる部分はあるのですが、大原則としてPowerShellはBashなど*shなシェルとはまるで異質のものです。

PowerShellと他のシェルを混同してしまっているため(他のシェルに対しての)期待した動作を得られず「PowerShellには"罠"がある。」と言われるケースを多く見てきました。

PowerShellの異質さ

PowerShellの異質さについては私が以前発表したスライドを見てもらうと理解して頂けるかと思います。

これらで触れている

PowerShellはAPIを志向している

PowerShellは.NET Frameworkのオブジェクトを扱うシェルである

といった点は他のシェルに慣れた方からするとかなり異質に感じることでしょう。
とはいえ、これはPowerShellを扱う上で必須の前提であり、この前提に立てば"罠"と感じることも減ると思います。*2

私的"罠"四天王

心理的要因とは関係なく普通にハマってしまうポイントも当然あります。
私自身もいくつもの"罠"を体験してPowerShellを学習してきました。

最初の項の内容とかぶる部分もありますが、私がハマってきたいくつかの"罠"を最後に紹介します。

1. 文字コード

2018年現在、PowerShell Coreが登場してPowerShellはクロスプラットフォームなアプリケーションとなっていますが、元々はWindowsの.NET Framework上で動作するアプリケーションでした。

このためPowerShellで取り扱える文字コードは.NET FrameworkおよびWindowsの影響を大きく受けています。

リダイレクト演算子やファイル操作に関わるコマンドレットの既定のエンコーディングがBOM付きUTF-16であることやUTF-8がBOM付きなのは、基本的には.NET Frameworkにおけるエンコーディングがそうである影響です。*3

また、非英語圏におけるエンコーディングは俗にANSIエンコーディングとも呼ばれ、OSのロケール設定に依存し日本語環境であればSHIFT-JIS(CP932)です。
PowerShellではコンソール上で扱える文字種に影響を及ぼしています。

コマンドプロンプトを常用する人であれば文字コードにSHIFT-JISを期待するでしょうし、Bashを常用する人であればBOM無しUTF-8を期待するでしょう。
PowerShellでは扱う文字コードを常に意識しておくと"罠"を回避できます。

2. -Pathパラメーターにおけるワイルドカード

これは罠を超えてPowerShell最大最悪の失敗仕様であり、多くの方にとってPowerShellへのヘイトを募らせる要因となっているかと思います。

PowerShellはコマンドプロンプトからの影響を受け、パラメーターに*?[]の4種の文字をワイルドカードとして使用できます。
それぞれ、

  • * : 0文字以上の任意の文字列
  • ? : 1文字の任意の文字
  • [ ] : []内で指定したパターンにマッチする文字列

となっています。

各コマンドレットがワイルドカード検索に対応するかどうかは一応任意なのですが、開発ガイドラインとしては

  • A cmdlet should support wildcard characters if possible.

  • The name of the parameter should be Path, with an alias of PSPath. Additionally, the Path parameter should support wildcard characters. If support for wildcard characters is not required, define a LiteralPath parameter.

となっており、Microsoft製のコマンドレットであれば-Name-Pathパラメーターはほぼ100%ワイルドカード検索をサポートしています。

ここで-Pathパラメーターが大問題で、Windowsの仕様では[]がディレクトリ・ファイル名として使用可能です。*4
またディレクトリ・ファイルを扱うコマンドレットにおいて-Pathパラメーターが既定となっており、例えば

mkdir .\[Test]
cd [Test]

なんてことをすると、ディレクトリ名にある[]を正しく認識できずエラーになってしまいます。
(ワイルドカードとして扱われてしまう)

この場合は

cd -LiteralPath [test]

の様に-LiteralPathを使うか

cd '.\`[Test`]\'

の様にワイルドカード文字をエスケープする必要があります。

この点に関しては、

ディレクトリ・ファイルを扱うコマンドレットでは原則 -LiteralPath パラメーターを使う。

ことを徹底するのをお勧めします。

3. 配列の平坦化 (パイプライン文)

PowerShellではコマンドレットなどの実行結果を変数に代入する際、実行結果のオブジェクトが複数ある場合はオブジェクトの配列(Object[])として扱われ、単一のオブジェクトの場合は配列ではなくそのオブジェクト自身を取得します。
挙動だけ見ると配列を期待した処理に対して平坦化が行われている様に見え、非常に直感的でない挙動に感じてしまうでしょう。

この挙動はパイプライン文(Pipeline statements)と呼ばれるPowerShell独自の構文に依るものです。
パイプライン文は名前の通りパイプライン(|)を扱うために用意された文なのですが、PowerShellのパイプラインは.NET Frameworkのオブジェクトを扱うオブジェクトパイプラインであり、他のシェルのそれとは完全に異質のものです。

わかりやすい表現をすると、これは厳密には間違っているのですが、PowerShellにおいてコマンドレットなどの実行結果は最終的にパイプライン文に行きつき、このパイプライン文は評価されたオブジェクトを一つずつストリーム(他のシェルの標準出力に近いもの)に放流する動作をします。
ストリームに放流されたオブジェクトはパイプライン演算子(|)でパイプすることができます。
放流されたオブジェクトは、途中で代入やパイプされなかった場合は最終的にPowerShell内部で| Out-Defaultと暗黙的にパイプされコンソールにその結果を表示する様になっています。

オブジェクトの取得(代入)はストリームに放流されたオブジェクトを途中で浚うイメージです。
(あくまでイメージであり正確な動作ではありません)
浚ったオブジェクトが単一であればそのまま扱い、複数個あれば配列(Object[])になる配列化が実際の挙動に近く、このため、必ず配列が欲しいといった場合は配列部分式演算子(@())などによる明示的なキャストが必要になります。

このパイプライン文の挙動は非常にわかりにくく、私もまだ厳密に正しい挙動を押さえることができていません...
ただ、パイプライン文やストリームといったものがあることを認識するだけでもPowerShellに対する理解はかなり変わると思います。

これらの詳細については本ブログの以下のエントリが役に立つでしょう。

blog.shibata.tech

blog.shibata.tech

加えて、PowerShellの関数もこのパイプライン文の影響下にあり独特の挙動をします。
こちらについては以下のエントリをご覧ください。

blog.shibata.tech

4. 配列に対する-eq演算子

最後の罠です。

blog.shibata.tech

で触れているのですが、配列に対して-eq演算子はフィルターとして動作します。

これを回避するには

if ($null -eq $array) {
    # do something
}

の様にヨーダ記法にする必要があります。

この点に関して私はハマる前に気が付けたので実害は被っていないのですが、罠としてはかなり凶悪な部類に入ると思います。

最後に

思うがままにPowerShellの"罠"について書いてきました。

PowerShellは他のシェルと比べて似て非なるツールであり誤解を生みやすい部分は残念ながらあります。
この点に関してPowerShellの設計は「悪い」としか言いようがありません。

しかしながら、PowerShellの前提・仕様を正しく押さえ、"罠"があることを知ってしまえば回避は容易です。

PowerShellは設計に悪い部分はありますが使いこなすと十分な実利を得ることができるツールだと私は思っています。
本エントリの内容が"罠"を回避してPowerShellで実利を得る一助になれば幸いです。

*1:時間があれば本ブログでもまとめたいですね...

*2:ただし仕様の理不尽さが無くなるとは言わないです...

*3:なお、最新のPowerShell Core 6.0ではクロスプラットフォーム化の影響もあり既定のエンコーディングはBOMなしUTF-8になっています

*4:*と?は不可