しばたテックブログ

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

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:*と?は不可

Azure Cloud ShellのPowerShellは起動時に何をしているのか

Azure Cloud ShellでPowerShellを起動した場合、通常のPowerShellの起動とは異なる初期処理が行われAzureドライブ(Azure:\で始まるドライブ)にロケーションが移動された状態で開始されます。

  • Cloud Shell(PowerShell)起動時

f:id:stknohg:20180524170552p:plain

  • Cloud Shell(Bash)からPowerShell Coreを起動した場合

f:id:stknohg:20180524170600p:plain

(PowerShellのコンテナでPowerShell Coreを起動しても同様)

本エントリではこの処理がどの様にして行われているのか解説します。

Cloud Shell(PowerShell)の場合

Cloud Shell(PowerShell)の場合、シェルの起動時に初期処理が行われています。
PowerShellの初期化処理といえばプロファイルに記述するのが一般的ですが、Cloud Shell(PowerShell)ではWindows PowerShellのプロファイルには何も記述されていません。

Cloud Shell(PowerShell)ではpowershell.exeの引数に初期化処理が記述されています。
powershell.exeの起動時引数は以下の様にすれば確認でき、

Get-CimInstance Win32_Process -Filter "processid = $PID" | Select-Object -ExpandProperty CommandLine

その結果

PS Azure:\> Get-CimInstance Win32_Process -Filter "processid = $PID" | Select-Object -ExpandProperty CommandLine
powershell.exe  -NoProfile -NoExit -NoLogo -command "Set-PSReadlineOption -TokenKind String -ForegroundColor Cyan; . ~\PSCloudShellStartup.ps1"

$env:USERPROFILE\PSCloudShellStartup.ps1を実行する様になっています。

また、PowerShell Coreのプロファイル(C:\Program Files\PowerShell\<version>\profile.ps1)は

#
# This profile script is used by PowerShell Core.
# Windows Powershell is started with -NoProfile to optimize its start time.
#

if (Test-Path $env:USERPROFILE\PSCloudShellStartup.ps1)
{
    . $env:USERPROFILE\PSCloudShellStartup.ps1
}

if (Get-Module -Name PSReadLine)
{
    # Set PSReadLine colors to be compatible with Cloud Shell UX requirements
    Set-PSReadlineOption -TokenKind String -ForegroundColor Cyan
}

と記述されており、こちらもPSCloudShellStartup.ps1を呼び出す様になっています。

Cloud Shell(Bash)の場合

Cloud Shell(Bash)の場合は、PowerShell Coreのプロファイル(/opt/microsoft/powershell/<version>/profile.ps1)に直接初期化処理が記述されており、その内容はPSCloudShellStartup.ps1と同一です。

初期化処理の詳細

現時点のPSCloudShellStartup.ps1(とprofile.ps1)の内容をGistに上げていますのでスクリプトの詳細はそちらで確認してください。

初期化処理は主に

  • Azure関連のモジュールの読み込みと認証
  • プロファイルパスの差し替え
  • prompt関数の差し替え
  • Azureドライブへの移動

を行っています。

この中でも特徴的なのがプロファイルパスの差し替えで、4つあるプロファイルの内

  • CurrentUserAllHosts
  • CurrentUserCurrentHost

のパスをそれぞれ

プロファイル Windows Ubuntu
CurrentUserAllHosts $env:USERPROFILE\CloudDrive\profile.ps1 $env:HOME/.config/PowerShell/profile.ps1
CurrentUserCurrentHost $env:USERPROFILE\CloudDrive\Microsoft.PowerShell_profile.ps1 $env:HOME/.config/PowerShell/Microsoft.PowerShell_profile.ps1

に差し替えて*1実行します。

加えて$PROFILE変数に付与されるNotePropertyからAllUsersAllHostsAllUsersCurrentHostを削除しています。
(正確には$PROFILE変数を上書き定義する際にNotePropertyの追加をしない様にしている)

Format-Listを使ってNotePropertyを確認すると以下の様になります。

PS Azure:\> $PROFILE | Format-List -Force

CurrentUserAllHosts    : C:\Users\ContainerAdministrator\CloudDrive\profile.ps1
CurrentUserCurrentHost : C:\Users\ContainerAdministrator\CloudDrive\Microsoft.PowerShell_profile.ps1
Length                 : 75

*1:細かい話をするとLinuxではパスが差し替わっていませんが、これはLinuxでは$env:HOME配下がCloud Drive上のVHDでマウントされ永続化対象となっているためです。Windowsではその様なマウントがないので永続化されるパスにプロファイルを差し替えています

「PowerShell実践ガイドブック」という本が発売されます

はい、宣伝です。

TL;DR;

ぎたぱそ先生が今月末にPowerShell Core 6.0の本を出版されます。
私もレビュアーとしてお手伝いしたので皆さんぜひ買ってくださいね。

PowerShell実践ガイドブック ~クロスプラットフォーム対応の次世代シェルを徹底解説~

PowerShell実践ガイドブック ~クロスプラットフォーム対応の次世代シェルを徹底解説~

「PowerShell実践ガイドブック」について

ぎたぱそ先生ことMVPの吉崎さんが「PowerShell実践ガイドブック」というタイトルのPowerShell Core 6.0を中心にPowerShellを解説する本を出版されます。

book.mynavi.jp

この本(以後本書と記載します)の内容についてはご本人のブログを見ていただくのが良いでしょう。

tech.guitarrapc.com

大変光栄なことに私にレビューの依頼があり、主にPowerShell Core 6.0の新機能や書籍の内容がPowerShellとして正しいかといった点を指摘、議論させていただきました。
私の他にも6名のレビュアーが様々な観点から本書をレビューしています。

僅かですが本書の内容に貢献できたことは嬉しくあり、また、私自身が新たな知識を得た部分もあり大変勉強となる良い経験ができました。

本エントリで改めてお礼申し上げます。
ありがとうございました。

そして出版おめでとうございます。

書評

レビュアーとして関わった以上べた褒めして終わりたいのですが、そこは抑えてできるだけ冷静に本書を評価したいと思います。

私の評価を一言でいうと、

本書は"PowerShell"というツールを学ぶには最適である、が、あなたを完全に満足させることは無いかもしれない。

です。

最適な点

本書は、発売される2018年5月末時点において、日本語で販売されている書籍の中では間違いなく一番詳細にPowerShellを解説しており、PowerShellの挙動や仕様を学ぶには最適な本であると断言できます。
その解説も丁寧で、PowerShellというツールの挙動や仕様を正しくおさえることでやりたいことを実践していくというスタンスになっています。

類似の書籍としては、かつて日本語版も出版されていた「PowerShell in Action」がありますが*1、吉崎さん自身も先述のブログで記載してる通り、

Windows PowerShell イン アクション も Windows PowerShell クックブックも何度か推薦するぐらい素晴らしい良著で私の大好きな本です。
 
PowerShell実践ガイドブックは、この2冊と明確に立ち位置が違います。
* PowerShell Core (現時点の最新である PowerShell 6.0) をメインとした内容になっている
* 体系的な基本機能も網羅しているが、常に実践を意識した内容になっている
* 実際にPowerShellを使うにあたっての操作や直面する課題を対象にしている

といった点が異なり、ある意味規格書*2に近いPowerShell in Actionより手に取りやすく読みやすい内容となっています。

PowerShell Core 6.0

また、本書は最新リリースであるPowerShell Core 6.0を想定環境として書かれていますが、バージョンに依らないPowerShellの機能にきちんと触れているので過去バージョンのPowerShellを扱っている方にとっても有用であり、特に問題を感じることなく読めるでしょう。

もちろんPowerShell Core 6.0の新機能について知りたい方にとっても、

  • 3章 3.2 リモーティング
  • 4章 4.9 クラス
  • 5章 5.4 .NETの相互運用
  • その他いくつかのコラム

あたりの内容は参考になると思います。

ただし、本書は機能の解説書ではないのでPowerShell Core 6.0の新機能一覧といったものはありません。
一覧が知りたければ、Docsにある

や本ブログの

を見てください。

本書はこれら一覧の理解を助けるものだと考えて頂ければ良いでしょう。

完全に満足させることは無いかもしれない、とは?

これはある意味本書の宿命的な部分であり、仕方ないのですが、書かないわけにはいかないので書きます。

本書は読者に

  • これからPowerShellを触ってみたい方(初学者)
  • システム管理者
  • 開発者

の三者を想定しています。

PowerShellはこのうちの「システム管理者」を想定して生まれたツールなのですが、現実には「開発者」やITリテラシーのさほど高くない「初学者」であるオフィスワーカーな方も使うことがごく普通にあります。
なのでこの想定自体は至極真っ当なのですが、はっきり言って間口が広い、あまりにも広い。

本書ではこの間口の広さに対応するために、最初の1~2章までは「初学者」にもわかる様に丁寧な記述を心掛け、以降の章では要求されるリテラシーが上がる構成となっています。
とはいえ、対象を絞らない万人に向く構成というのはあるわけが無く、読者のレベル感によって「最初の章の説明が冗長すぎる」逆に「後半の章の前提知識が難しすぎる」といったギャップが出てしまう可能性は残念ながらあると思っています。

ただ、想定読者と実際の読者のレベルのギャップというのは本書に限らずどの技術書でも出てしまう問題だと思います。
もしかしたら私が気にしすぎているだけかもしれません。

本書は間口の広い書籍であり、どの層の読者が読んでも得るものがあると断言できるのですが、その逆にどの層の読者にも満たされない部分が残ってしまうのではないかと危惧しており、それが「あなたを完全に満足させることは無いかもしれない」と言った理由になります。

ただしこの点については、

この本は過去の「PowerShellを触り始めた直後の自分」がほしかった本でもあり、今の自分が幅広い利用者を想定して書き出せる限界ギリギリを絞りました。

と言われている様に、できるだけ幅広い層に届く様にと敢えて意図した選択であることは補足しておきます。

本書はガイドブックでもあります。
もしこの様なギャップを感じることがあれば、読者自身のレベル感に応じて必要な部分をつまんで読み進めるのが良いでしょう。

個人的な一押しポイント

あと、私が個人的にお勧めできるポイントを紹介します。

  • 2章 2.8 パイプライン
  • 2章 2.9 フォーマッティング
  • 4章 4.5 スコープ

の内容は、わかりやすい解説がこれまでの書籍やWEB上でも少なかったので、本書の解説がきっと役に立つと思います。
特にスコープに関しては私自身体系立ててまとめることが出来ていなかった部分があり、本書をレビューしたことで理解を深めることができました。
加えて、

  • 5章 5.1 実世界のPowerShell
  • 5章 5.2 困ったときに

は本書のタイトル通り実践に即したものですのできっと読者の役に立つと思います。

最後に

ちょっとネガティブなことも書きましたが、本書はPowerShellを学ぶ上で間違いなく役に立ちます。

本ブログをご覧になる方はPowerShellに興味がある方が多いと思いますので、ぜひ一度は本書を手に取ってみて欲しいですし、できれば買って頂けると嬉しいです。


PowerShell実践ガイドブック ~クロスプラットフォーム対応の次世代シェルを徹底解説~

PowerShell実践ガイドブック ~クロスプラットフォーム対応の次世代シェルを徹底解説~

*1:残念ながら日本語版(初版)は絶版です...

*2:PowerShell in Actionの著者であるBruce PayetteさんはPowerShell Teamの人でありPowerShell founderの一人です