しばたテックブログ

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

PowerShell既定のフォーマット設定に介入する方法について (補足)

前回のエントリに対する補足を軽く書いておきます。

blog.shibata.tech

format.ps1xmlのDefaultSettingsタグ

PowerShell既定のフォーマット設定に介入するには、適当なforamt.ps1xmlファイルを作ってPropertyCountForTableタグに更新したい値を設定すればよいと説明しました。

設定例)

<?xml version="1.0" encoding="utf-8" ?>
<Configuration>
    <DefaultSettings>
        <PropertyCountForTable>2</PropertyCountForTable>
    </DefaultSettings>
</Configuration>

このタグの親であるDefaultSettingsですが、これはPowrerShellのフォーマット設定の既定値に関するタグとなりPropertyCountForTable以外にも複数のタグがあることが確認できました。

ただし、どのタグも通常は一切使われていないため何のためのものか・設定するとどういった効果があるかは正直謎です...

謎なのですが、せっかくなので見つけたタグについてわかる範囲で補足しておきます。

ShowErrorタグ および DisplayErrorタグ

どちらのタグもBool型の値を取り、既定値はFalseでした。

ShowErrorタグの説明*1には、

if true, display error messages

DisplayErrorタグの説明には

if true, display an error string in the formatted display (e.g. cell in a table)

と記載されておりフォーマットの内部処理でエラーが発生した際にエラー情報を出すか否かを制御するのに使われる様です。
恐らくデバッグ用の項目でしょう。

EnumerableExpansions -> EnumerableExpansion -> Expandタグ

階層が深いのですがExpandタグ以外のタグは無い様です。
このタグはMicrosoft.PowerShell.Commands.Internal.Format.EnumerableExpansion列挙型となり以下の様に定義されています。

internal enum EnumerableExpansion
{
    /// <summary>
    /// process core only, ignore IEumerable
    /// </summary>
    CoreOnly,

    /// <summary>
    /// process IEnumerable, ignore core
    /// </summary>
    EnumOnly,

    /// <summary>
    /// process both core and IEnumerable, core first
    /// </summary>
    Both,
}

既定値はEnumOnlyです。
名前からして配列などのオブジェクトの展開方法に関わっている様ですが、具体的に何をしているのかさっぱりわかりませんでした...

今後わかることがあれば追記します。

設定例

最後にこれらのタグの設定例を記載しておきます。

<?xml version="1.0" encoding="utf-8" ?>
<Configuration>
    <DefaultSettings>
        <ShowError>False</ShowError>
        <DisplayError>False</DisplayError>
        <EnumerableExpansions>
            <EnumerableExpansion>
                <Expand>EnumOnly</Expand>
            </EnumerableExpansion>
        </EnumerableExpansions>
    </DefaultSettings>
</Configuration>

*1:正確には対となるオブジェクトのプロパティのコメント

PowerShell既定のフォーマット設定に介入する方法について

きっかけはカナダのMVP、Thomas Raynerさんのブログエントリから。

thomasrayner.ca

PowerShellにおいて予め*.format.ps1xmlによって表示書式が定められてないオブジェクトは、表示されるプロパティの数が4個以下の場合はTableフォーマットを、5個以上になる場合はListフォーマットを取ります。*1

例としてプロパティの数が異なる簡単なPSCustomObjectを挙げます。

# PS 3.0以降の環境で再現可能
[PSCustomObject]@{Prop1=1;Prop2=2}
[PSCustomObject]@{Prop1=1;Prop2=2;Prop3=3}
[PSCustomObject]@{Prop1=1;Prop2=2;Prop3=3;Prop4=4}
[PSCustomObject]@{Prop1=1;Prop2=2;Prop3=3;Prop4=4;Prop5=5}

このコードを実行すると下図の様にプロパティの数が5になった時点でListフォーマットに変化していることが分かります。

f:id:stknohg:20171006030945p:plain

このプロパティ数の根拠は何なのか?

このはなしに関しては、どのドキュメントだったかは失念したのですが、以前にも聞いたことがありました。

当時は「そんなものなのかなぁ...」となんとなく流していたのですが今はPowerShellはオープンソースとなっています。
せっかくなのでソースからこのプロパティ数の根拠を調べてみました。

調査したソースのバージョンは現時点の最新であるPowerShell 6.0 Beta7ですが、PowerShellの根幹にかかわる部分ですので過去のバージョンでもさほど変わらないと思います。

調査結果

結果としては、Microsoft.PowerShell.Commands.Internal.Format.FormatViewManagerクラスのこのあたりが怪しそうです。

それっぽいコメントが記載されています。

// Microsoft.PowerShell.Commands.Internal.Format.FormatViewManagerクラスより抜粋

// we did not get any default view (and shape), we need to force one
// we just select properties out of the object itself, since they were not
// specified on the command line
_viewGenerator = SelectViewGeneratorFromProperties(shape, so, errorContext, expressionFactory, db, null);

で、このSelectViewGeneratorFromPropertiesメソッドは内部で、Microsoft.PowerShell.Commands.Internal.Format.DisplayDataQueryクラスのGetShapeFromPropertyCountメソッドを呼んでいます。
名前からして正解に近そうです。

// Microsoft.PowerShell.Commands.Internal.Format.FormatViewManagerクラスより抜粋
private static ViewGenerator SelectViewGeneratorFromProperties(FormatShape shape, PSObject so,
                            TerminatingErrorContext errorContext,
                            MshExpressionFactory expressionFactory,
                            TypeInfoDataBase db,
                            FormattingCommandLineParameters parameters)
{
    // いろいろ省略

    // decide what shape we want for the given number of properties
    shape = DisplayDataQuery.GetShapeFromPropertyCount(db, expressionList.Count);

    // 後略

このGetShapeFromPropertyCountメソッドの定義は以下の様になり、

// Microsoft.PowerShell.Commands.Internal.Format.DisplayDataQuery クラスより抜粋
internal static FormatShape GetShapeFromPropertyCount(TypeInfoDataBase db, int propertyCount)
{
    if (propertyCount <= db.defaultSettingsSection.shapeSelectionDirectives.PropertyCountForTable)
        return FormatShape.Table;

    return FormatShape.List;
}

表示対象となるプロパティの数が

db.defaultSettingsSection.shapeSelectionDirectives.PropertyCountForTable

以下であればTableフォーマット(FormatShape.Table)、そうでなければListフォーマット(FormatShape.List)を選ぶ様になっています。

いい感じです。
PropertyCountForTableの内容を調べれば正解にたどり着けそうです。

これはMicrosoft.PowerShell.Commands.Internal.Format.ShapeSelectionDirectivesクラスに定義されており、以下の様になっています。

// Microsoft.PowerShell.Commands.Internal.Format.ShapeSelectionDirectives クラスより抜粋
internal int PropertyCountForTable
{
    set
    {
        if (!_propertyCountForTable.HasValue)
        {
            _propertyCountForTable = value;
        }
    }
    get
    {
        if (_propertyCountForTable.HasValue)
            return _propertyCountForTable.Value;
        return 4;
    }
}
private int? _propertyCountForTable;

_propertyCountForTableに値が定義されてればその値を使い、値が定義されていない場合は4を返す様になっています。

詳細は後述しますが通常_propertyCountForTableには値がセットされておらず4を使う様になっています。
プロパティ数の4の根拠はここの様です。

PowerShell既定のフォーマット設定に介入する方法

ここから本エントリの本題に入ります。

先ほど判明したPropertyCountForTableプロパティおよび_propertyCountForTableについてですが、さらに調査をしたところ、PowerShellのオブジェクトの書式定義を行う.format.ps1xmlファイルから設定可能であることが分かりました。

既定の.format.ps1xmlファイルはPSHOMEディレクトリに複数用意されており、また、PowerShell 5.1からはこのファイルと対になる内部クラスを使う様に置き換えられていますが、これらのファイル・クラスを調べてもPropertyCountForTableプロパティを設定している個所はありませんでした。
このため先に述べた様に既定値の4が使われています。

.format.ps1xmlファイルはユーザーが独自に定義することが可能です。
独自に作成した.format.ps1xmlファイルからPropertyCountForTableプロパティに介入できないか試してみたところ、このプロパティの値を変えることができました。

手順は次の通りです。

最初に、以下の様なXMLファイルを作成して任意の名前で保存します。
PropertyCountForTableタグの値がPropertyCountForTableプロパティに指定する値となります。(今回は2にしています)

<?xml version="1.0" encoding="utf-8" ?>
<Configuration>
    <DefaultSettings>
        <PropertyCountForTable>2</PropertyCountForTable>
    </DefaultSettings>
</Configuration>

このXMLファイルをUpdate-FormatDataコマンドレットで読み込んでやればPropertyCountForTableプロパティの値を更新できます。

# XMLファイルの名称を sample.format.ps1xml とした場合
# -PretendPathでも大丈夫ぽい
Update-FormatData -AppendPath .\sample.format.ps1xml

実行例は以下の様になりフォーマットが変化するプロパティの数が2に変わっていることがわかります。

f:id:stknohg:20171006031006p:plain

ちなみに、本エントリはPowerShell 5.1の環境で動作確認していますが、PowerShell 2.0の環境でも介入可能でした。

最後に

とりあえずこんな感じです。
PowerShellがオープンソース化してくれたおかげで以前はわからなかった挙動に対する理解を深めることができました。

すばらしいですね。


【2017/10/06追記】

補足を書きました。

blog.shibata.tech

*1:標準で表示書式が定められているオブジェクトは設定された書式が優先されこのルールに従いません

PowerShellでmkdirしたディレクトリにcdする方法

クラスメソッドさんの

dev.classmethod.jp

が人気なので便乗してPowerShellだとどうすれば良いか軽く書いておきます。

本エントリの内容はWindows 10、PowerShell 5.1、PSReadline 1.2な環境で動作確認しています。

1. $$自動変数を使う

Bashでは$_が「ひとつ前に実行したコマンドラインの最後の引数」となっていますが、PowerShellで$_は「パイプラインで現在処理されているオブジェクト」を表す自動変数なので使えません。

PowerShellでは代わりに$$を使います。
$$は正確には「セッションが受け取った最後の行にある最後のトークン」なのですが、コマンド実行直後であれば「ひとつ前に実行したコマンドラインの最後の引数」と同じに使えます。

実行例はこんな感じです。

mkdir .\very\very\long-directory
cd $$

2. パイプラインでつなぐ

PowerShellではmkdir関数に成功すると作成されたディレクトリオブジェクト(System.IO.DirectoryInfo)を返します。
このため、この値をパイプラインでつなぎcd(Set-Location)に引き渡すことが可能です。

mkdir .\very\very\long-directory | cd

ただ、この方法でロケーションを移動した場合、プロンプトに表示されるロケーションが、

PS Microsoft.PowerShell.Core\FileSystem::C:\very\very\long-directory > 

の様に先頭にPSProvider(Microsoft.PowerShell.Core\FileSystem::の部分)が修飾されてしまう場合があり、見た目としては非常によろしくありません...

一応、

mkdir .\very\very\long-directory | % { $_.FullName } | cd

# or

mkdir .\very\very\long-directory | cvpa | cd

のように一旦%(ForEach-Object)や、cvpa(Convert-Path)をはさむことで回避はできるのですが、冗長で本末転倒です...

3. cd → Alt + . (PSReadline Windowsキーバインド)

PSReadlineを個別にインストールするか、標準でPSReadlineがインストール済みのWindows 10/Windows Server 2016環境でこの方法を使うことができます。

PSReadlineのデフォルトのキーバインドではAlt + .YankLastArgという機能で割り当てられており、「ひとつ前に実行したコマンドラインの最後の引数」を取得することができます。
このため、cdAlt + .の順に入力することで直近に入力したパスを補完することが可能です。

なお、キーバインドの割り当ては以下のコマンドで確認することができます。

Get-PSReadlineKeyHandler | ? { $_.Function -eq "YankLastArg" }

結果

Key   Function    Description
---   --------    -----------
Alt+. YankLastArg Copy the text of the last argument to the input

4. cd → ESC → . (PSReadline Emacsキーバインド)

PSReadlineではSet-PSReadlineOptionによりキーバインドを変更することができます。

Set-PSReadlineOption -EditMode Emacs

とすることでキーバインドをEmacs風にすることができ、この場合、cdESC.の順に入力すれば「ひとつ前に実行したコマンドラインの最後の引数」を取得することができます。

ちなみに、

Get-PSReadlineKeyHandler | ? { $_.Function -eq "YankLastArg" }

の結果は以下の様に変更されています。

Key      Function    Description
---      --------    -----------
Alt+.    YankLastArg Copy the text of the last argument to the input
Alt+=    YankLastArg Copy the text of the last argument to the input
Escape,. YankLastArg Copy the text of the last argument to the input
Escape,= YankLastArg Copy the text of the last argument to the input

5. ESC → k 0 cw cd (PSReadline viキーバインド)

PSReadlineはVer.1.2からvi風のキーバインドをサポートしています。

Set-PSReadlineOption -EditMode Vi

とするこでキーバインドをvi風にすることができます。
あとは元記事同様に、ESCk 0 cw cdで前回のコマンド呼び出し文字列置換を行う事ができます。

6. mkdir と cd を同時に実行する関数をあらかじめ定義しておく

独自の関数を作るのであれば好きなように作ってしまうのが良いと思います。

mkdir自体がNew-Itemコマンドレットをラップした関数なので、本格的な関数を作りたい人はmkdirの定義を確認してみると良いでしょう。
役に立つ情報を得ることができるはずです。

本エントリでは元記事に近い形の簡易な関数を紹介するにとどめておきます。

function mkdir-cd {
    mkdir $args[0] | cvpa | cd
}

※なお、PowerShellでは&&は予約語となっていますが、サポートされておらず使えません。

最後に

とりあえずこんな感じです。

BashやZshほどではないですがPowerShellでもいろいろな方法を選べますので是非試してみてください。