しばたテックブログ

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

Visual Studio Codeで新規作成したファイルの言語モードを設定する方法

なんとなく試したら上手くいってしまったので。
現在のバージョン(Ver.1.9)では上手くいきましたが、今後新しいバージョンではできなくなるかもしれません...

【2017/04/06追記】Ver.1.11以降の方法

本日更新されたVer.1.11でsettings.jsonに新たにfiles.defaultLanguageというパラメーターが追加されデフォルトの言語を設定できる様になりました。

例えばデフォルトをPowerShellにしたい場合は、

// settings.json
{
    // ※すべて小文字で powershell とすること
    "files.defaultLanguage": "powershell"
}

とすればファイルを新規作成した時点の言語モードがPowerShellになってくれます。
わかりやすい設定が増えてくれて嬉しい限りです。

以降の内容についてはVer.1.11以前のやり方ということで残しておきます。

動機

私はVisual Studio CodeをほぼPowerShellを書くのに使っているため、ファイルを新規作成した時点で言語モードがPowerShellになってくれていると便利なんだけどだなぁというのが動機です。

公式な手順

公式に新規作成したファイルの言語モードを設定する方法はない様で(あれば教えてください)、最初は必ずプレーンテキストになっています。

f:id:stknohg:20170203181741p:plain

この状態から、

  • 右下の言語モードの表示欄をクリック
  • Ctrl + K → M

のどちらかの手順で言語モードを変更する必要があります。

f:id:stknohg:20170203181801p:plain


f:id:stknohg:20170203181807p:plain

新規作成したファイルの言語モードを設定する

都度言語モードを変更するのは面倒だったので、ダメ元でユーザー設定(settings.json)に以下の設定を入れてみたところ、新規作成したファイルの言語モードをPowerShellにすることができてしまいました。

// settings.json
"files.associations": {
    "Untitled-*": "powershell"
}

本来files.associationsは拡張子に応じた言語モードを設定する場所なのですが、ファイル名自体も判定の対象になっている様でUntitled-*の様な設定も許されます。
新規作成した時点のファイル名はUntitled-1,Untitled-2,Untitled-3...といった名称なので、これに合わせた設定にすることで言語モードを変えることができました。

このおかげでファイルを新規作成した時点で直ちにPowerShell拡張の機能(インテリセンスなど)を利用できる様になり、非常に便利になりました。

f:id:stknohg:20170203182229p:plain

Windows Management Framework (WMF) 5.1がリリースされました

長く苦しい戦いだった...

公式のアナウンスは以下。

blogs.msdn.microsoft.com

更新内容について

リリースノートはこちら

WMF 5.1の新機能ついてはこちらかこのブログの以下のエントリを参照してください。

インストール可能OSやインストール方法など

WMF 5.1をインストール可能なOSは以下。

  • Windows 7 SP1 / Windows Server 2008 R2 SP1
  • Windows Server 2012
  • Windows 8.1 / Windows Server 2012 R2

詳細はこちらを、インストール方法はこちら*1を参照してください。

プレビュー版の時は.NET Framework 4.6(+Windows 7/Windows Server 2008 R2の場合はWMF 4.0の事前インストール)が必要でしたが、この条件はリリース版では変更され、.NET Framework 4.5 4.5.2*2のみ必要(Windows 7/Windows Server 2008 R2の場合はWMF 4.0の事前インストール不要)になりました。

【おまけ】Windows 7 SP1にインストールしてみる

せっかくなのでWindows 7 SP1(64bit版)にWMF 5.1をインストールしてみます。

0. はじめに

これまでのWMFはmsuファイルで更新が提供されていましたが、WMF 5.1ではWindows 7 SP1 / Windows Server 2008 R2 SP1に対してはmsuでなくzipファイルで提供されています。
(その他のOSについてはmsuファイルのままです。)

このzipファイルにはmsuファイルに加えInstall-WMF5.1.ps1というスクリプトが同梱されており、このInstall-WMF5.1.ps1を実行することでインストールを行います。  
Install-WMF5.1.ps1ではインストール前に実行環境のチェックを行い次の内容を検証しています。

  • インストール可能なOSであること
  • .NET Framework 4.5以上がインストールされていること
  • WMF 3.0がインストールされていないこと

これらのチェックに失敗するとエラーメッセージとともにインストールが中断されます。

例) .NET Framework 4.5がインストールされていない場合

f:id:stknohg:20170120203112p:plain

1. zipのダウンロード

Win7AndW2K8R2-KB3191566-x64.ZIPをダウンロードして適当なディレクトリに解凍します。
今回はC:\Temp\Win7AndW2K8R2-KB3191566-x64に解凍しました。

f:id:stknohg:20170120202825p:plain

2. Install-WMF5.1.ps1の実行

必要があればスクリプトの実行を可能にしてください。

Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process

f:id:stknohg:20170120202945p:plain

あとはInstall-WMF5.1.ps1を実行するだけです。
実行するとmsuファイルが実行され更新のインストールが開始されます。

.\Install-WMF5.1.ps1

f:id:stknohg:20170120203239p:plain

f:id:stknohg:20170120203250p:plain

f:id:stknohg:20170120203303p:plain

f:id:stknohg:20170120203330p:plain

インストール後に再起動を求められます。

f:id:stknohg:20170120203636p:plain

再起動すればインストール完了です。

f:id:stknohg:20170120205041p:plain

なおPSReadlineは付いていませんので必要でしたらInstall-Moduleでインストールしてください。

Install-Module PSReadline -Scope CurrentUser

3. サイレントインストール

また、Install-WMF5.1.ps1は以下の2つの引数を取ることができサイレントインストールも可能となっています。

  • AcceptEULA : EULAに自動で同意する
  • AllowRestart : 自動で再起動を許可する

実行例)

.\Install-WMF5.1.ps1 -AcceptEULA -AllowRestart

f:id:stknohg:20170120210347p:plain

*1:まだ日本語版は情報が古いので英語版へのリンクを貼っています

*2:.NET Framework 4.5は既にサポート切れのため4.5.2を要求する様にドキュメントなどが差し替えられています。WMF5.1のインストール自体は4.5でも可能です。

PowerShellのHashtableがコレクション扱いされない話

先日Twitter上でちょっと話題になってたのでメモを残しておきます。

PowerShellのHashtableはコレクション扱いされない

こちらは割と既知の話で、

pierre3.hatenablog.com

winscript.jp

にある通りPowerShellのHashtableはコレクション扱いされません。

例えばC#で以下の様に書けるコードが

// C# (LinqPad)
var hash = new System.Collections.Hashtable();
hash.Add("Key1", "Value1");
hash.Add("Key2", "Value2");
foreach (DictionaryEntry item in hash)
{
    Debug.WriteLine(string.Format("{0} 型の値 {1}={2} が渡されました。", item.GetType(), item.Key, item.Value));
}

f:id:stknohg:20170119023537p:plain

PowerShellでは異なる挙動をします。
foreachだけでなくパイプラインも同様です。

# 適当なHashTable
$hash = @{
    key1 = "Value1";
    key2 = "Value2"
}
# C#とは違い、PowerShellではforeach文でHashTableの要素を列挙できない
foreach ($item in $hash) {
    Write-Host ("{0} 型の値 {1}={2} が渡されました。" -f $item.GetType(), $item.Key, $item.Value)
}

# パイプラインでも同様に要素を列挙できない
$hash | ForEach-Object { Write-Host ("{0} 型の値 {1}={2} が渡されました。" -f $_.GetType(), $_.Key, $_.Value) }

f:id:stknohg:20170119023858p:plain

Hashtableの各要素を列挙可能にするにはGetEnumerator()メソッドを使ってやる必要があります。

# 列挙するには GetEnumerator() メソッドを使う
foreach ($item in $hash.GetEnumerator()) {
    Write-Host ("{0} 型の値 {1}={2} が渡されました。" -f $item.GetType(), $item.Key, $item.Value)
}

# パイプラインも同様
$hash.GetEnumerator() | ForEach-Object { Write-Host ("{0} 型の値 {1}={2} が渡されました。" -f $_.GetType(), $_.Key, $_.Value) }

f:id:stknohg:20170119024043p:plain

Dictonaryもコレクション扱いされない

そして、この挙動はDictionaryを使った場合も同様になります。

$dict = New-Object "System.Collections.Generic.Dictionary[string, string]"
$dict.Add("Key1", "Value1")
$dict.Add("Key2", "Value2")
# C#とは違い、PowerShellではforeach文でDictionaryの要素を列挙できない
foreach ($item in $dict) {
    Write-Host ("{0} 型の値 {1}={2} が渡されました。" -f $item.GetType(), $item.Key, $item.Value)
}

# パイプラインでも同様に要素を列挙できない
$dict | ForEach-Object { Write-Host ("{0} 型の値 {1}={2} が渡されました。" -f $_.GetType(), $_.Key, $_.Value) }

f:id:stknohg:20170119024408p:plain

こちらも列挙可能にするにはGetEnumerator()メソッドを使う必要があります。

# 列挙するには GetEnumerator() メソッドを使う
foreach ($item in $dict.GetEnumerator()) {
    Write-Host ("{0} 型の値 {1}={2} が渡されました。" -f $item.GetType(), $item.Key, $item.Value)
}

# パイプラインでも同様
$dict.GetEnumerator() | ForEach-Object { Write-Host ("{0} 型の値 {1}={2} が渡されました。" -f $_.GetType(), $_.Key, $_.Value) }

f:id:stknohg:20170119024641p:plain

では何が列挙可能(Enumerable)なのか?

PowerShellのコレクションにおいて何が列挙可能かについて仕様書等を調べてみましたが、具体的に明記されたものを見つけることができませんでした。*1

仕方ないので現時点の最新のソースコードを調べたところ、System.Management.Automation.LanguagePrimitivesクラス(その1その2)でどの型が列挙可能(Enumerable)かを判定してると思われる部分を見つけたので以下に転記しておきます。

// LanguagePrimitives.cs より

        private static void InitializeGetEnumerableCache()
        {
            lock (s_getEnumerableCache)
            {
                // PowerShell doesn't treat strings as enumerables so just return null.
                // we also want to return null on common numeric types very quickly
                s_getEnumerableCache.Clear();
                s_getEnumerableCache.Add(typeof(string), LanguagePrimitives.ReturnNullEnumerable);
                s_getEnumerableCache.Add(typeof(int), LanguagePrimitives.ReturnNullEnumerable);
                s_getEnumerableCache.Add(typeof(double), LanguagePrimitives.ReturnNullEnumerable);
            }
        }

// ・・・中略・・・

        private static GetEnumerableDelegate CalculateGetEnumerable(Type objectType)
        {
#if !CORECLR
            if (typeof(DataTable).IsAssignableFrom(objectType))
            {
                return LanguagePrimitives.DataTableEnumerable;
            }
#endif

            // Don't treat IDictionary or XmlNode as enumerable...
            if (typeof(IEnumerable).IsAssignableFrom(objectType)
                && !typeof(IDictionary).IsAssignableFrom(objectType)
                && !typeof(XmlNode).IsAssignableFrom(objectType))
            {
                return LanguagePrimitives.TypicalEnumerable;
            }

            return LanguagePrimitives.ReturnNullEnumerable;
        }

この記述によれば、

  1. System.Data.DataTableクラス*2
  2. System.StringSystem.Collections.IDictionaryインターフェイス、System.Xml.XmlNodeを除いたSystem.Collections.IEnumerableインターフェイス

が列挙可能の様でこれまでの挙動とも一致します。

// Don't treat IDictionary or XmlNode as enumerable...

のコメントの通り、Hashtableを含むIDictionaryは意図的に列挙できない様にされている事がわかります。

ちなみに、HashtableやDictonaryでGetEnumerator()メソッドを使うと列挙可能になるのは、このメソッドの戻り値がHashtableEnumeratorEnumerator型になりIDictionaryインターフェイスが無くなるためです。

おまけ

DataTableも列挙可能*3ということなので最後に動作確認をしてみます。

$table = New-Object 'System.Data.DataTable' -ArgumentList ('Table1')
$c1 = $table.Columns.Add('Code',[string])
$c2 = $table.Columns.Add('Name',[string])
$c3 = $table.Columns.Add('Age',[int])
$table.PrimaryKey = $c1
[void]$table.Rows.Add("001", "山田", 20)
[void]$table.Rows.Add("002", "田中", 25)

# C#とは異なりDataTableがそのままforeachで使える
foreach ($row in $table) {
    Write-Host ("{0} 型の値 {1},{2},{3} が渡されました。" -f $row.GetType(), $row.Code, $row.Name, $row.Age)
}

# パイプラインも列挙可能
$table | ForEach-Object { Write-Host ("{0} 型の値 {1},{2},{3} が渡されました。" -f $_.GetType(), $_.Code, $_.Name, $_.Age) } 

f:id:stknohg:20170119030336p:plain

確かに各行(DataRow)が列挙可能になっています。

*1:PowerShell in Actionにちょっとそれっぽい事は書いていましたが...

*2:Core Editionを除く

*3:DataTable.Rowsの各要素を取得できます