しばたテックブログ

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

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

PowerShell Remoting over SSHを試す - 再び

以前に試した

blog.shibata.tech

ですが、PowerShell Core 6.0が正式リリースされ当時の内容では正しく動作しなくなっていたので改めてやり直しました。

PowerShell Remoting over SSHとは

従来のPowerShell Remoting Protocol(PSRP)では通信の下回りにHTTP/HTTPSを使いますが、PowerShell Remoting over SSHでは名前の通りSSHを使った通信を行います。

基本的な仕組みとしては、

  1. クライアント側はpowershell(.exe)がリモートセッション生成時にssh(.exe)を呼び出し、powershell(.exe)→ssh(.exe)のプロセス間通信を経由してサーバーへの通信を行う。

  2. サーバー側はSSHdのサブシステムにPowerShellを登録。
    クライアントからの接続があった場合はsshd(.exe)→powershell(.exe)とリモートシェルが実行される。

となっています。

PowerShell Remoting over SSHを試す

手順はGitHubで公開されているのでこの手順をベースに行います。
https://github.com/PowerShell/PowerShell/tree/master/demos/SSHRemotinggithub.com

【2018/05/20更新】
いつのまにかGitHubの手順が削除されてしまいました... (#6628)
手順は一致しないかもしれませんが、Docsのこちらの手順が参考になりますので代わりにご覧ください。

もしくは削除直前のこちらのバージョンをご確認ください。

試験環境

試験環境はVirtualBox上でWindows Server 2012 R2とCentOS 7.4.1708を使い双方向で通信させてみます。
他のOSでも細かい部分に違いはあるでしょうが基本的には同じ流れになると思います。

その他細かい条件は以下。

項目 Windows Server 2012 R2 CentOS 7.4.1708
基本設定 インストール直後の状態+最新のWindows Updateを実施 bento/centos7.4のBoxにyum updateを実施
IP 192.168.33.210 192.168.33.209
ユーザー vagrant, administrator vagrant, root

Windows → CentOSへの通信(パスワード認証)

CentOS側の設定

CentOSにPowerShell Coreをインストールして、sshd_configの設定を変更します。
パスワード認証、鍵認証の可否については試験環境のデフォルト設定のままとしました。

# CentOS PowerShell for Linuxのインストール
curl https://packages.microsoft.com/config/rhel/7/prod.repo | sudo tee /etc/yum.repos.d/microsoft.repo
sudo yum install -y powershell

# /etc/ssh/sshd_config の設定
# '# override default of no subsystems'のコメント行の下に設定を追加
# ※位置指定をかなり決め打ちにしているので注意
sudo sed -i.orig -e "/# override default of no subsystems/a Subsystem powershell /usr/bin/pwsh -sshs -NoLogo -NoProfile" /etc/ssh/sshd_config

# sshdの再起動
sudo systemctl restart sshd.service

Windows側の設定

標準でインストールされているPowerShell 4.0ではPowerShell Remoting over SSHできませんので、こちらにもPowerShell Coreをインストールします。
前回と比べてGitHubへの接続にTLS 1.2が必須となったため、Windows PowerShell側の設定を若干変更する必要があります。

# Windows PowerShell 4.0
# GitHubへ接続するためにTLS 1.2を有効化
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12

# PowerShell Coreのインストール
Invoke-WebRequest -Uri "https://github.com/PowerShell/PowerShell/releases/download/v6.0.1/PowerShell-6.0.1-win-x64.msi" -OutFile "PowerShell-6.0.1-win-x64.msi"
Start-Process -FilePath msiexec.exe -ArgumentList @("/i", "$($pwd.Path)\PowerShell-6.0.1-win-x64.msi", "/passive") -Wait -PassThru

次にSSHをインストールします。 Zipの解凍をしたいので先ほどインストールたPowerShell Coreから以下の手順でインストールします。

# PowerShell Core 6.0.1
# ssh.exeのインストール 
Invoke-WebRequest -Uri "https://github.com/PowerShell/Win32-OpenSSH/releases/download/v7.6.0.0p1-Beta/OpenSSH-Win64.zip" -OutFile "OpenSSH-Win64.zip"
Expand-Archive -Path ".\OpenSSH-Win64.zip" -DestinationPath "C:\"
[Environment]::SetEnvironmentVariable('PATH', [Environment]::GetEnvironmentVariable('PATH') + ';C:\OpenSSH-Win64', 'Machine')

インストール先はC:\OpenSSH-Win64としましたが、こちらについては必要に応じて変更しても構いません。
また、PowerShell Remoting over SSHではssh.exeに対してPATHが通っていること(正確にはssh.exeだけでコマンド呼び出しができること)が必須であるためPATHは必ず通す様にしてください。

テスト接続

ここから実際に試してみます。

まずはNew-PSSessionでPSセッションを作成します。 -HostName(-ComputerNameではない)、-UserNameパラメーターを指定するとPowerShell Remoting over SSHで通信することになります。

# PowerShell Core
$Session = New-PSSession -HostName 192.168.33.209 -UserName vagrant

実行すると以下の様にssh.exeを使ってサーバーに接続する際のあれやこれやを聞かれるので適切な情報を入力していきます。

f:id:stknohg:20180312193552p:plain

入力後、エラーが出なければ成功です。
$Sessionの中身を確認すると以下の様になり、名前がSSHになっていることがわかります。
このセッションがssh.exeと紐づいています。

f:id:stknohg:20180312193616p:plain

この$sessionを使いEnter-PSSessionしてリモート接続します。

# PowerShell Core
Enter-PSSession -Session $Session

実行結果は以下の様になり、無事CentOSにリモート接続できました。

f:id:stknohg:20180312193640p:plain

リモートからClear-Hostが使えないのは相変わらずでした...
TERM環境変数を適当に設定してみましたが、Clear-Hostしてもエスケープシーケンスが表示されるだけなのでOSが対応しないとダメそうな雰囲気を感じます。
(Windows10やWindows Server 2016だと上手くいくやも?)

最後にRemove-PSSessionしてセッション情報を削除すると紐づいていたssh.exeも終了します。

# PowerShell Core
Remove-PSSession -Session $Session
  
# 個別に消すのが面倒なら以下で一気に消しても良い
Get-PSSession | Remove-PSSession

Windows → CentOSへの通信(鍵認証)

鍵認証で通信する場合、基本的にSSHの鍵認証の方法と同一です。

先ずはWindows側でssh-keygen.exeで鍵を作ります。
鍵の種類やパスフレーズの有無は適当にしてください。

以下の実行例ではRSA2 4096Byte、パスフレーズ有りにしています。

# Windows PowerShell 4.0 / PowerShell Core どちらでも可
#  作成する鍵の設定は適当に
cd C:\OpenSSH-Win64\
.\ssh-keygen.exe -t rsa -b 4096

f:id:stknohg:20180312193705p:plain

あとは作った公開鍵(id_rsa.pub)をCentOS側に転送し~/.ssh/authorized_keysに追加するだけです。 以下の例では/vagrant/id_rsa.pubに鍵を転送しています。

# CentOS bash
# ユーザーはvagrantユーザー
cat /vagrant/id_rsa.pub >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys

これで鍵認証の準備は完了です。
鍵認証で接続する場合はNew-PSSession-KeyFilePathパラメーターを使用します。

実行例は以下

# PowerShell Core
$Session = New-PSSession -HostName 192.168.33.209 -UserName vagrant -KeyFilePath ~\.ssh\id_rsa

f:id:stknohg:20180312193737p:plain

セッションが生成された後はパスワード認証の場合と同様です。

CentOS → Windowsへの通信(パスワード認証)

最後に、CentOSをクライアント、Windowsをサーバーにした場合の接続を試します。
前回同様パスワード認証のみとします。

まずはWindows上でsshdを動作させます。 基本的にはGitHub上のインストール手順に従うだけなので問題ないかと思います。

# Windows PowerShell 4.0
cd C:\OpenSSH-Win64\

# sshdのインストール
.\install-sshd.ps1

# Firewall設定
New-NetFirewallRule -Name sshd -DisplayName 'OpenSSH Server (sshd)' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22

# サービス起動
Start-Service sshd

sshd_configの設定はCentOSの場合と同様でサブシステムにPowerShell(Coreの方)を登録します。
なお、sshd_configC:\ProgramData\ssh\sshd_configを修正してください。(これはVer.1.0.0.0-betaからの変更になります)

追加例 )

# C:\ProgramData\ssh\sshd_config

# override default of no subsystems の欄に追記
Subsystem   sftp    sftp-server.exe
Subsystem powershell c:/program files/powershell/6.0.1/pwsh.exe -sshs -NoLogo -NoProfile

あとパスワード認証も許可しておきます。

# コメント解除
PasswordAuthentication yes

サービスを再起動して設定を反映させます。

Restart-Service sshd

これで準備は完了です。

CentOSからWindowsへ接続してみます。

# CentOS PowerShell Core
$Session = New-PSSession -HostName 192.168.33.210 -UserName administrator

セッション情報はこんな感じになります。

f:id:stknohg:20180312193850p:plain

Enter-PSSessionでWindowsへ接続できます。

# CentOS PowerShell Core
Enter-PSSession -Session $Session

f:id:stknohg:20180312193909p:plain

前回同様にPowerShell Core→PowerShell 4.0(Desktop)とネストしてPowerShellを対話的に実行できないままで、powershell.exe -Command {ほげ}の様にコマンド実行であればネストして実行可能でした。

PowerShellの三項演算子について

あまりにも今更なはなしなのですが、根強い需要があるみたいなのでざっくりブログに書いておきます。

【2019/09/20追記】PowerShell 7の三項演算子

本日PowerShell 7 Preview.4がリリースされ、このバージョンの新機能の一つとして三項演算子が導入されました。
詳細を別エントリーにまとめていますのでこちらも併せてご覧ください。

dev.classmethod.jp

PowerShellの三項演算子

はじめに結論を書いておくと、現在、PowerShellに三項演算子はありません。
PowerShell 1.0 ~ PowerShell Core 6.0に至るまでどのバージョンでもサポートされていません。

ただ、三項演算子に対する要望はPowerShellがリリースされた当初からあった様で、オープンソース化した現在も以下のIssueで要望と提案がなされています。

github.com

私自身は三項演算子に対する思い入れは全くないので気にしていませんが、気になる方はこちらのIssueに積極的に参加していくと良いでしょう。

三項演算子の代わりになるもの

で、これで終わってしまうとあまりにも芸がないので三項演算子の代わりになる方法をいくつか紹介します。

1. if文

一般的なプログラミング言語に慣れている方には受け入れ難い動作かもしれませんが、PowerShell 2.0からif文が値を返す動作をします。

このため、

$result = if($condition){ Write-Output "True" }else{ Write-Output "False" }

の様な記述を三項演算子の代わりに使うことができます。
なお、式の中でこれを使いたい場合は、

"Result is " + $(if($condition){ Write-Output "True" }else{ Write-Output "False" })

の様に部分式($())にしてやればOKです。

イメージとしてはVB.NETのif演算子が近いでしょうか。


余談ですが、PowerShellにおける文と式の曖昧さ、そしてパイプライン文については牟田口先生のこちらの考察をご覧ください。必見です。

winscript.jp

2. 2値配列

ショートコーディングのテクニックの一つなのですが配列で三項演算子を代用することができます。

スクリプト中で使うことはあまりお勧めしませんがちょっとしたワンライナーを書くときのテクニックとして覚えておいて損はないかと思います。

以下の様に2値の配列にTrue、Falseごとの処理を記述し、配列の要素指定に条件を記述します。

$result = &({ Write-Output "True" }, { Write-Output "False" })[!$condition]

[int]$false = 0[int]$true = 1と暗黙的に型変換されることを利用してそれぞれの条件を実行するテクニックです。

3. Invoke-Ternary

最初に触れたPowerShell Team Blogの記事からの引用です。

# 定義
filter Invoke-Ternary ([scriptblock]$decider, [scriptblock]$ifTrue, [scriptblock]$ifFalse) 
{
    if (& $decider) { 
        & $ifTrue
    } else { 
        & $ifFalse 
    }
}
Set-Alias ?: Invoke-Ternary -Option AllScope -Description "PSCX filter alias"

# 使用例
1..10 | ?: {$_ -gt 5} {"Greater than 5";$_} {"Not greater than 5";$_}

関数やフィルターを作ってしまうならコレにこだわらず自分の好きな様に作ってしまうのが良いでしょう。

最後に

ほかにも色々なやり方で三項演算子を代用することができると思いますが本エントリではこのくらいにしておきます。