しばたテックブログ

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

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:標準で表示書式が定められているオブジェクトは設定された書式が優先されこのルールに従いません

Redmine 3.4.2でrake redmine:migrate_from_tracをエラー無く動作させるためのパッチを書きました

タイトル通りです。

Trac LightningからRedmineへ移行するにあたり、公式?に用意されていた移行ツールがRedmine 3.4.2ではエラーが出て動かなかったので、とりあえずエラー無く動作させるパッチを書いてみました。

Redmine 3.4.2でrake redmine:migrate_from_tracをエラー無く動作させるためのパッチ

パッチはGistに上げています。

Redmine 3.4.2でrake redmine:migrate_from_tracをエラー無く動作させるためのパッチ

gist.github.com

パッチの詳細や使い方についてはコメントに書いてあります。

Tracからのデータ移行に関して

RedmineでTracからデータ移行をするにはrake redmine:migrate_from_tracコマンドを使います。
だだ、このコマンドはTrac 0.11あたりを対象としており、作られた時期もかなり昔の様です。
残念ながら現在はコードのメンテナンスが行われておらず、何も考えず実行すると以下の様にエラーになってしまいます。

f:id:stknohg:20170930120311p:plain

出ているエラーはRedmine内部で使用しているRailsのバージョンが上がったことが原因であるため、本エントリのパッチは単純にエラーとなっている個所を新しいRailsに合わせた形に直しているだけとなります。

パッチを当てた後で再度rake redmine:migrate_from_tracを実行すると下図の様にウィザードを進めることができ、エラー無くデータを移行することができます。

f:id:stknohg:20170930120350p:plain

f:id:stknohg:20170930120411p:plain

注意事項

Ver.1.0以降の新しいTracだとデータの内部形式が異なる様で、本パッチを適用しても「データベースが見つからない」旨のエラーが出てしまいます。
新しいバージョンのTracからデータを移行したい場合は気合いでなんとかするしかない様です。

私の場合、Trac Lightningは新しいバージョンでもTrac 0.12だったので運よくパッチだけで済みました...

このツールに関して、エラーに関するIssueも幾つか出ているのですが、中の人たちの優先度も低い様で改善される見込みは無いと思われます。
(仮に私が中の人だとしても間違いなく優先度は低くするでしょうし、むしろこのツールをサクッと捨てると思います...)

また、Gistにも記載していますが、Trac 0.12のデータを移行する際は、

migrate_from_trac does not support trac 0.12

のパッチも適用しておく必要があります。
これはTrac 0.12から時刻データの内部保持形式が変わった(UnixTimeからマイクロ秒単位のUnixTimeに変更されている)のに対応するパッチとなっています。

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でもいろいろな方法を選べますので是非試してみてください。