しばたテックブログ

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

PowerShell Core 6.0をデバッグ実行する - Visual Studio 2017編

前回の続き的な。

blog.shibata.tech

前回の時点では「まだVisual Studio 2017でのビルドはうまく動作しない」と説明しましたが、つい最近Visual Studioで動作させるための修正が完了したので実際に試してみました。

github.com

はじめに

基本的なところは前回と同様です。
IDEがVisual Studio CodeからVisual Studio 2017になっただけと考えて差し支えないでしょう。

ただ、詳細は後述しますが、Visual Studioだけでビルドに関するすべての手順を賄えるわけではなく少しだけPowerShellのコマンドを併用する必要があります。

PowerShellをデバッグ実行する

ここから具体的な手順の説明に入ります。

1. Gitのインストール

ソースコードをCloneするのにGitクライアントが必要です。
適当にインストールしてgitコマンドが使える様にしておいてください。

gitforwindows.org

ちなみに、Visual Studioをインストールした後にVisual Studio Installerから「Git for Windows」を選択してインストールしても構いません。

2. ソースのClone

PowerShellのリポジトリよりソースコードをCloneします。

今回はとりあえずデバッグ実行できれば良いので前回同様にリポジトリを直接Cloneしておきます。

任意のディレクトリで以下の様に--recursiveオプションを付けてgit cloneしてください。

git clone --recursive https://github.com/PowerShell/PowerShell.git

3. Visual Studio 2017のインストール

www.visualstudio.com

Visual Studio 2017をダウンロードしてインストールしてください。

私はProfessionalとEneterpriseで動作確認しましたがどのエディションでも大丈夫だと思います。
手元のVisual Studioは結構昔にインストール済みだったので具体的なインストール手順は紹介できませんが、

  • .NETデスクトップ開発 - C#をコンパイルできる標準的な構成
  • .NET Coreクロスプラットフォームの開発 - 要.NET Core SDK

のワークロードがインストールされていれば良いと思います。

なお、PowerShell Coreで必要な.NET Core SDKのバージョンは随時更新されていますので、場合によってはこちらから個別にダウンロードしてインストールする必要があるかもしれません。

本エントリを書いている時点では、PowerShell Core 6.1.0-preview.1をコンパイルするのに.NET Core SDK 2.1.4が必要です。

4. その他ツールのインストール

上記の他に

  • PowerShell 6.0の実行バイナリ(pwsh)
  • RCEdit

が必要になります。

こちらは前回と同様ですので、build.psm1モジュールのStart-PSBootstrap関数を使ってインストールしておけば良いでしょう。

# cd [PowerShellのルートディレクトリ]
cd .\PowerShell\
Import-Module .\build.psm1
Start-PSBootstrap

5. ビルドとデバッグ実行

CloneしたソースのルートディレクトリにPowerShell.slnというソリューションファイルがあるのでVisual Studioからこれを開きます。

f:id:stknohg:20180412001744p:plain

14のプロジェクトから成るソリューションで、Windowsの場合powershell-win-coreプロジェクトのProgram.csにエントリポイントがあります。


この初期状態で単純にソリューションのビルドをしても下図の様に幾つかエラーがでてしまいます。

f:id:stknohg:20180412001839p:plain

これはソースをCloneした直後の状態ではメッセージ等のリソースファイルが存在していないため、リソース不足によるコンパイルエラーとなってしまうためです。

リソースはVisual Studioからは作成できず*1build.psm1モジュールのStart-PSBuild関数を使う必要があります。

次の様にbuild.psm1モジュールをインポートし、-ResGenパラメータを指定してStart-PSBuildを実行してください。

# cd [PowerShellのルートディレクトリ]
cd .\PowerShell\
Import-Module .\build.psm1
Start-PSBuild -ResGen

エラー無く処理が終われば必要なリソースは作成されています。

f:id:stknohg:20180412001923p:plain


リソースが出来ている状態でビルドすればエラー無く完了するはずです。

f:id:stknohg:20180412002020p:plain

デバッグを開始してやれば下図の様にふつうにステップ実行できます。

f:id:stknohg:20180412002047p:plain

最後に

Visual Studio Codeも悪くはありませんがやっぱりVisual Studioの圧倒的な支援は最高です。
ほんとうに捗ります。

*1:もしできる方法があれば教えてください...

PSCoreUpdateというPowerShellモジュールを公開しました

個人的な不便さからPowerShell Coreのアップデートを自動化するためのモジュールを作りました。
ソースと基本的な使い方はGitHubに上げています。

github.com

作った動機

現在、Windows環境においては一度インストールしたPowerShell Core 6.0のアップデートを自動で行う方法は無く、毎回新しいインストーラを手動でダウンロードして実行しなおす必要があります。
一応Office等のMicrosoft製ソフトと同様にMicrosoft Update経由での自動アップデートに対応する計画が立てられている(#6118)のですが、実現までにはまだまだ時間がかかりそうです。

また、MacOS環境においてはHomebrew Caskによる更新が可能ですが、そもそも論としてHomebrewのインストール自体が割と手間なためHomebrewに頼らない更新方法が欲しかったというのもあります。

インストール方法

PowerShell Galleryからインストール可能です。

Install-Module PSCoreUpdate -Scope CurrentUser

なお、このモジュールはあくまで更新のためのツールなので最初にPowerShell Core 6.0をインストールするのは手動で行う必要があります。

使い方

このモジュールには4つのコマンドがあり、各コマンドの使い方をざっくり説明していきます。

Update-PowerShellCore

新しいバージョンのPowerShell Coreがリリースされている場合、インストーラーのダウンロードと自動実行を行います。
Windowsの場合はMSIファイル、MacOSの場合はPKGファイルをダウンロードして実行します。

新しいバージョンが無い場合は何もしませんが、-Forceパラメーターを指定することで過去バージョンのインストールを強制することも可能です。

# 実行例
Update-PowerShellCore -Latest

実行イメージはこんな感じです。

-Silentパラメーターを指定することでサイレントインストールにすることも可能です。

# 実行例
Update-PowerShellCore -Latest -Silent

なお、このコマンドはWindowsとMacOS専用です。
Linux環境においてはAptやYum等の標準のパッケージ管理ツールで十分との判断をしています。

Test-LatestVersion

現在のコンソールが最新バージョンかを判定します。

# 実行例
Test-LatestVersion

最新バージョンの場合

f:id:stknohg:20180408222143p:plain

最新バージョンでない場合

f:id:stknohg:20180408222340p:plain

Find-PowerShellCore

GitHubからPowerShell Coreのリリース情報を取得します。
パラメーターによるバージョン指定も可能です。

# 実行例
Find-PowerShellCore -Latest

f:id:stknohg:20180408222355p:plain

内部的にはGitHub REST APIを使っており、使用回数にレートリミットが設けれられています。
通常であれば問題にならないはずですが、レートリミットに引っかかる様な場合は-Tokenパラメーターにアクセストークンを指定することで回避可能にしています。

# 実行例
Find-PowerShellCore -Latest -Token $env:GITHUB_ACCESS_TOKEN

Save-PowerShellCore

PowerShell Coreのリリースアセット(MSIファイルなど)をダウンロードします。
Download-PowerShellCoreにエイリアスしています。

Save-PowerShellCore -Latest -AssetType MSI_WIN64 -OutDirectory C:\Temp

アセットの種類は以下となっています。

種類 内容
MSI_WIN32 [PowerShell version]-win-x86.msi
MSI_WIN64 [PowerShell version]-win-x64.msi
PKG_OSX1011 [PowerShell version]-osx.10.11-x64.pkg
PKG_OSX1012 [PowerShell version]-osx.10.12-x64.pkg
RPM_RHEL7 [PowerShell version]-rhel.7.x86_64.rpm
DEB_DEBIAN8 [PowerShell version]-debian.8_amd64.deb
DEB_DEBIAN9 [PowerShell version]-debian.9_amd64.deb
DEB_UBUNTU14 [PowerShell version]-ubuntu.14.nn_amd64.deb
DEB_UBUNTU16 [PowerShell version]-ubuntu.16.nn_amd64.deb
DEB_UBUNTU17 [PowerShell version]-ubuntu.17.nn_amd64.deb
APPIMAGE [PowerShell version]-x86_64.AppImage
TAR_LINUXARM32 [PowerShell version]-linux-arm32.tar.gz
TAR_LINUX64 [PowerShell version]-linux-x64.tar.gz
TAR_OSX [PowerShell version]-osx-x64.tar.gz
ZIP_WINARM32 [PowerShell version]-win-arm32.zip
ZIP_WINARM64 [PowerShell version]-win-arm64.zip
ZIP_WIN32 [PowerShell version]-win-x86.zip
ZIP_WIN64 [PowerShell version]-win-x64.zip

【2018/04/23追記】 初期セットアップスクリプト

初期セットアップスクリプトを作りました。
詳しくは下記エントリをご覧ください。

blog.shibata.tech

AutomationNullとは何なのか?

ちょっと前のはなしなのですがこちらのGistを読みました。

こちらのGistではPowerShellを使う上でハマりがちな罠について詳しく記述されておりPowerShellに対する非常に良い批判となっています。

で、このGistの中で$nullAutomationNullの2つのNULLについて触れられています。
私自身AutomationNullの存在は知っていたのですが表に露出しない内部的なものだと認識しており、これまで全然気にかけていませんでした。

本エントリではこのAutomationNullについて分かる範囲で説明したいと思います。

お断り

本エントリの内容はあくまで私個人の調査結果でありPowerShellとして公式な何かを表明するものではありません。

極力間違いの無い様に調べた結果を書いていますが内容に誤りがある可能性は多分にあります。
その点は割り引いてご覧ください。

あと本エントリに対するフィードバックは大歓迎です。
おかしな点などありましたら是非お知らせください。

$nullとは何なのか

現在PowerShellはオープンソースとなっています。
$nullAutomationNullの2つのNULLについて知るにはその実装を見てみるのが一番手っ取り早いでしょう。

最初に$nullについてその実装を見てみます。
PowerShellにおいてNULLを表現する変数である$nullSystem.Management.Automation.NullVariableに定義されています。

/// ShellVariable.cs(Ver.6.0.1) より引用

/// <summary>
/// This class is used for $null.  It always returns null as a value and accepts
/// any value when it is set and throws it away.
/// </summary>
///
internal class NullVariable : PSVariable
{
    /// <summary>
    /// Constructor that calls the base class constructor with name "null" and
    /// value null.
    /// </summary>
    ///
    internal NullVariable() : base(StringLiterals.Null, null, ScopedItemOptions.Constant | ScopedItemOptions.AllScope)
    {
    }

    /// <summary>
    /// Always returns null from get, and always accepts
    /// but ignores the value on set.
    /// </summary>
    public override object Value
    {
        get
        {
            return null;
        }

        set
        {
            // All values are just ignored
        }
    }

    /// <summary>
    /// Gets the description for $null.
    /// </summary>
    public override string Description
    {
        get { return _description ?? (_description = SessionStateStrings.DollarNullDescription); }
        set { /* Do nothing */ }
    }
    private string _description;

    /// <summary>
    /// Gets the scope options for $null which is always None.
    /// </summary>
    public override ScopedItemOptions Options
    {
        get { return ScopedItemOptions.None; }
        set { /* Do nothing */ }
    }
}

Valueプロパティが常にnullを返し、入力はすべて破棄される様になっています。
$nullはPowerShellにおいて.NET Frameworkのnullを表現するものであることがわかります。

ちなみにこの変数のDescriptionには

PS C:\> (Get-Variable 'null').Description
References to the null variable always return the null value. Assignments have no effect.

References to the null variable always return the null value. Assignments have no effect.

と記述されています。

AutomationNullとは何なのか?

そしてAutomationNullSystem.Management.Automation.AutomatinNullに定義されています。

/// AutomationNull.cs(Ver.6.0.1) より引用

/// <summary>
/// This is a singleton object that is used to indicate a void return result.
/// </summary>
/// <remarks>
/// It's a singleton class. Sealed to prevent subclassing. Any operation that
/// returns no actual value should return this object AutomationNull.Value.
/// Anything that evaluates an MSH expression should be prepared to deal
/// with receiving this result and discarding it. When received in an
/// evaluation where a value is required, it should be replaced with null.
/// </remarks>
public static class AutomationNull
{
    #region private_members

    // Private member for Value.

    #endregion private_members

    #region public_property

    /// <summary>
    /// Returns the singleton instance of this object.
    /// </summary>
    public static PSObject Value { get; } = new PSObject();

    #endregion public_property
}

ソースを見る限りいわゆるNULLオブジェクトとして定義されており非常にシンプルな内容です。
実装から見えるものはほとんど無いですが、summaryとremarksタグのコメントに非常に重要なことが書かれています。

AutomationNullはPowerShellにおけるVoidである

summaryコメントにAutomationNullの用途がズバリ書き記されています。

This is a singleton object that is used to indicate a void return result.

indicate a void return resultとある様にAutomationNullはPowerShellにおける式や文を評価した際に戻り値がないことを表すクラスであり、いわゆるVoid型と言われるものに相当します。

そしてremarksコメントに

Any operation that returns no actual value should return this object AutomationNull.Value.
Anything that evaluates an MSH expression should be prepared to deal
with receiving this result and discarding it. When received in an
evaluation where a value is required, it should be replaced with null.

とあり、AutomationNull.Valueを受け取った文や式はその値を切り捨てるべきとされ、値が必要とされる場合はnull(=$null)に置き換えられる様に要請されています。


AutomationNullがVoidであるわかりやすい例として、[void]型(System.Void型)へのキャストはAutomationNull.Valueを返す実装となっている点が挙げられます。

/// LanguagePrimitives.cs(Ver.6.0.1) より一部引用

    public abstract class PSTypeConverter
    {
        // ・・・ (省略) ・・・

        //
        // [void]型へのキャストは AutomationNull.Value を返す様になっている
        //
        private static object ConvertToVoid(object valueToConvert,
                                            Type resultType,
                                            bool recursion,
                                            PSObject originalValueToConvert,
                                            IFormatProvider formatProvider,
                                            TypeTable backupTable)
        {
            typeConversion.WriteLine("returning AutomationNull.Value.");
            return AutomationNull.Value;
        }

        // ・・・ (省略) ・・・

        private static ConversionData FigureLanguageConversion(Type fromType, Type toType,
                                                               out PSConverter<object> valueDependentConversion,
                                                               out ConversionRank valueDependentRank)
        {

            // ・・・ (省略) ・・・

            if (toType == typeof(void))
            {
                return CacheConversion<object>(fromType, toType, LanguagePrimitives.ConvertToVoid, ConversionRank.Language);
            }

            // ・・・ (省略) ・・・
        }

        // ・・・ (省略) ・・・
    }

このほかにも、その実装までは紹介できませんが、AutomationNullはパイプラインに渡されず破棄される、ストリームの出力も無視されるなどの挙動が確認できており、これがAutomationNullがVoidであることの裏付けになっていると私は考えています。

$nullとAutomationNullの違い

ここまででPowerShellに2つのNULLである$nullAutomationNullが存在している理由については明らかにできたかと思います。

これまで私はAutomationNullについてはあくまで内部用で表には露出しないと思っていたのですが、実態は先のGistにある様に当たり前の様に露出します。
(できるだけユーザーに対して見えない様にしていると感じますが、見えないけど確実に存在する状態が現状と思えます)

それどころか、何気なくPowerShellを使い$nullだと思っているもののほとんどが実はAutomationNull.Valueでした。

これは私の予想ですが、PowerShellにおけるNULLはAutomationNullが基本であり、$nullは.NET Frameworkの機能を呼び出すとき等でNULL値を明示する必要がある際に使う特殊な値と認識するのが正解の様です。

ただ、AutomationNullはユーザーに対して見えない様にしたいがために全くドキュメント化されておらず$nullと同じに見える挙動さえします。

この様な現状に対しては正直批判も止む無しと思いますが、直ちにどうにかすることができないのも現実なので割り切りは必要です。

$nullとAutomationNullの挙動の違い

ここから$nullAutomationNullの挙動の違いについて触れます。

原則として、

  • AutomationNullはVoidであり、評価を打ち切るものである
  • $nullは.NET Frameworkのnullを明示するものである

という役割の違いが挙動の違いに表れています。

先述のGistの例を出すと、

# NULL値を明示している
> @($null).Length
1

これはNULL値を明示的に利用する必要があるため要素数1の配列となります。

# AutomationNull.Valueの評価は打ち切られる
> @([System.Management.Automation.Internal.AutomationNull]::Value).Length
0

こちらは配列部分式演算子(@())をAutomationNull.Valueに対して評価することになり、評価を打ち切る必要があるため空の配列となります。

こちらの記述だと違和感があるかもしれませんが、以下の様な記述であれば0を返すことにさほど違和感を感じないのではないかと思います。

# [void]キャストはAutomationNull.Valueを返す
> @([void](Get-Date)).Length
0

補足資料

本エントリでの説明はこれくらいにしておきたいと思います。

AutomationNullの挙動についてはGitHubのIssueにもいくつか挙げられおり、その是非について広く議論されています。
最後にいくつかIssueを紹介しますので興味があればぜひご覧ください。

github.com

github.com

github.com