からめもぶろぐ。

俺たちは雰囲気で OAuth をやっている

platyPS で PowerShell ヘルプ ファイルを簡単に書く

PowerShell チームから提供されている platyPS というツールがあります。

github.com

ざっくりいうと、既存のモジュールを読み込んで Markdown 形式のテンプレートを生成し、さらに Markdown ファイルを読み込んで PowerShell ヘルプ ファイル (dll-Help.xml) を生成してくれるというものです。ドキュメントを Markdown 形式で管理できるので、ドキュメントの記述が容易ですし、GitHub に公開しておけばそのままオンライン ドキュメントとしても使うことができます。

Markdown ファイルを作成する

こんなコマンドレットを作ったとして。

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Management.Automation;
using System.Text;

namespace SampleModule
{
    [Cmdlet("Write", "HelloWorld")]
    public class WriteHelloWorldCommand : PSCmdlet
    {
        protected override void ProcessRecord()
        {
            this.WriteObject(JsonConvert.SerializeObject(new { Message = "Hello World" }));
        }
    }
}

対象のモジュールをインポートしてから New-MarkdownHelp を呼び出すとコマンドレットごとに Markdown ファイルを生成してくれます。

Import-Module -Name '.\bin\Debug\SampleModule.psd1'
New-MarkdownHelp -Module 'SampleModule' -OutputFolder '.\documents\'

生成された Markdown ファイルにはモジュールから読み取った情報が自動的に埋め込まれているものの、説明などは含まれていません。{{ }} で囲まれている箇所について修正していきます。

---
external help file: SampleModule.dll-Help.xml
Module Name: SampleModule
online version:
schema: 2.0.0
---

# Write-HelloWorld

## SYNOPSIS
{{Fill in the Synopsis}}

## SYNTAX

```
Write-HelloWorld [<CommonParameters>]
```

## DESCRIPTION
{{Fill in the Description}}

## EXAMPLES

### Example 1
```powershell
PS C:\> {{ Add example code here }}
```

{{ Add example description here }}

## PARAMETERS

### CommonParameters
This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable.
For more information, see about_CommonParameters (http://go.microsoft.com/fwlink/?LinkID=113216).

## INPUTS

### None

## OUTPUTS

### System.Object
## NOTES

## RELATED LINKS

ヘルプ ファイルを作成する

修正が終わったら Markdown ファイルがあるフォルダーを指定して New-ExternalHelp を呼び出してヘルプ ファイルを生成します。

New-ExternalHelp -Path '.\documents\' -OutputPath '.\bin\Debug\SampleModule.dll-Help.xml'

これだけです。簡単ですね。

コマンドレットの変更を Markdown に反映する

開発を進めていくうちにコマンドレットに新しいパラメーターが増えたりすることもあると思います。そのような場合は Update-MarkdownHelp を呼び出すといい感じに差分を更新してくれます。*1

Import-Module -Name '.\bin\Debug\SampleModule.psd1'
Update-MarkdownHelp -Path '.\documents\'

修正したらまた New-ExternalHelp でヘルプ ファイルを再生成します。なお既存のファイルが存在すると上書きされないので -Force を付けるのを忘れずに。

*1:New-MarkdownHelp と Update-MarkdownHelp で微妙にパラメーターが違うので注意。どうしてこうなった。

AppVeyor で PowerShell Core モジュールのバージョンを書き換える

こんな記事を書いてました。

blog.karamem0.jp

これでもよかったのですが、せっかくなのでビルドするごとにバージョン番号を書き換えたいよね、ということで。

AppVeyor にはビルド時にアセンブリ バージョンを書き換える機能が提供されています。.NET Framework だけではなく .NET Core にも対応しています。詳しくは公式のドキュメントを見るのがいいと思います。

www.appveyor.com

アセンブリ バージョンについてはそれでいいのですが、PowerShell Core モジュールの場合、psd1 ファイルも書き換えてあげる必要があります。こちらはさすがに自力でやらないといけないので、ビルド スクリプトを書いてみます。

dotnet restore --source https://api.nuget.org/v3/index.json
dotnet publish --configuration Release
Update-ModuleManifest -Path "${env:APPVEYOR_BUILD_FOLDER}\bin\Release\netcoreapp2.1\publish\${env:APPVEYOR_PROJECT_NAME}.psd1" -ModuleVersion $env:APPVEYOR_BUILD_VERSION

ファイル パスは環境に合わせて適宜書き換えてみてください。Update-ModuleManifest で既存の psd1 ファイルの内容を更新できますので、AppVeyor の環境変数からバージョン番号を受け取って設定してあげます。例えば appveyor.yml ファイルに以下のように書いておけば、マイナー バージョンやメジャー バージョンを上げるときにも一元管理できます。

version: '1.0.0.{build}'

そういえば、まったくの余談ですが、PowerShell Core のライブラリは NuGet からも入手できるようになってたんですね。

www.nuget.org

SharePoint Online Management Shell が PowerShell Gallery から取得できる件

以前にこんな記事を書きました。

blog.karamem0.jp

その後 SharePoint Online Management Shell については PowerShell Gallery からもインストールできるようになったようです。半年近く前の話なのに全然気付きませんした…。

techcommunity.microsoft.com

インストーラー版の SharePoint Online Management Shell は GAC を汚染するので、特別な事情がなければ、なるべく PowerShell Gallery (Install-Module) で取得したほうがいいですね。

Binary Module (C#) でいい感じに Write-Verbose したい

この記事は「PowerShell Advent Calendar 2018」の参加記事です。

qiita.com

ちなみに投稿が遅れたのではなく 16 日が埋まっていなかったので穴埋めしています。ここ重要!

Write-Verbose について

PowerShell では Verbose パラメーターを付けることでトレース情報を出力させることができます。モジュールの内部では Write-Verbose を呼び出すことで出力内容を制御します。Script Module の場合、PowerShell のスクリプトからコマンドレットを呼び出すだけなので、特に何も考える必要はないのですが、Binary Module の場合は勝手が違います。なぜなら、Cmdlet.WriteVerbose メソッドは static ではないからです。単純なコマンドレットであれば、ProcessRecord メソッドで全部書けば済むかもしれません。ある程度の規模になってくるとそうもいきません。かといって Cmdlet のインスタンスを持ち回すのも美しいとは思えません。

Trace.WriteLine があるじゃないか

C# には Trace.WriteLine メソッドがあります。こいつを使えばいけるのでは…と思いましたが、標準では Trace.WriteLine をホストに流し込んでくれるような仕組みにはなっていないようです。*1 仕方がないので TraceLister を自作します。

CmdletTraceListener.cs

public class CmdletTraceListener : TraceListener
{
    private readonly Cmdlet cmdlet;

    public CmdletTraceListener(Cmdlet cmdlet)
    {
        if (cmdlet == null)
        {
            throw new ArgumentNullException(nameof(cmdlet));
        }
        this.cmdlet = cmdlet;
    }

    public override void Write(string message)
    {
        this.cmdlet.WriteVerbose(message);
    }

    public override void WriteLine(string message)
    {
        this.cmdlet.WriteVerbose(message);
    }
}

TraceableCmdlet.cs

public abstract class TraceableCmdlet : PSCmdlet
{
    private readonly TraceListener traceListener;

    protected TraceableCmdlet()
    {
        this.traceListener = new CmdletTraceListener(this);
    }

    protected override void BeginProcessing()
    {
        Trace.Listeners.Add(this.traceListener);
        base.BeginProcessing();
    }

    protected override void EndProcessing()
    {
        base.EndProcessing();
        Trace.Listeners.Remove(this.traceListener);
    }
}

こうしてあげることで、どこからでも Trace.WriteLine を呼び出すだけで (Verbose パラメーターを指定していれば) ホストにメッセージが出力されますし、疎結合になるのでテストも容易になるのではないかと思います。

(2018/12/20 追記) Twitter でご指摘いただきました。ありがとうございます。

確かに例外が発生すると EndProcessing が呼ばれないので、TraceListener が解除されずにメモリ リークになってしまっているようです。IDisposable を実装してあげるのがよいようですね。

tech.blog.aerie.jp

(2019/04/19 追記) パイプライン使ったときに駄目みたいですねorz

stackoverflow.com

*1:Console.WriteLine は Write-Host と同様に動作します。

PowerShell Core で Binary Module (C#) を開発するときの注意点について

この記事は「PowerShell Advent Calendar 2018」の参加記事です。

qiita.com

サンプルとして簡単な JSON を返すコマンドレットを持つモジュールを作成します。対象の PowerShell Core のバージョンは 6.1.0 です。

サンプル コード

SampleModule.csproj

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netcoreapp2.1</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
    <PackageReference Include="System.Management.Automation" Version="6.1.0" />
  </ItemGroup>
  <ItemGroup>
    <None Update="SampleModule.psd1">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
  </ItemGroup>
</Project>

SampleModule.psd1

@{
    RootModule = 'SampleModule.dll'
    ModuleVersion = '1.0.0'
    CmdletsToExport = "*"
}

WriteHelloWorldCommand.cs

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Management.Automation;
using System.Text;

namespace SampleModule
{
    [Cmdlet("Write", "HelloWorld")]
    public class WriteHelloWorldCommand : PSCmdlet
    {
        protected override void ProcessRecord()
        {
            this.WriteObject(JsonConvert.SerializeObject(new { Message = "Hello World" }));
        }
    }
}

実行結果

dotnet publish してから Import-Module でモジュールを読み込んで実行してみますがエラーになります。

C:\SampleModule> dotnet publish
C:\SampleModule> Import-Module "C:\SampleModule\bin\Debug\netcoreapp2.1\publish\SampleModule.psd1"
C:\SampleModule> Write-HelloWorld
Write-HelloWorld : Could not load file or assembly 'Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed'. Could not find or load a specific file. (Exception from HRESULT: 0x80131621)
At line:1 char:2
+  Write-HelloWorld
+  ~~~~~~~~~~~~~~~~
+ CategoryInfo          : NotSpecified: (:) [Write-HelloWorld], FileLoadException
+ FullyQualifiedErrorId : System.IO.FileLoadException,SampleModule.WriteHelloWorldCommand

何が起こっているのか?

PowerShell Core は自身が Json.NET をライブラリとして使っています。PowerShell Core 6.1 系では Json.NET 11.0.2 が同梱されています。PowerShell Core を起動した時点で 11.0.2 がロードされてしまっているので、インポートしたモジュールで異なるバージョンのアセンブリを読もうとするとエラーになってしまいます。この問題を解決するには、モジュールが使う Json.NET のバージョンを常に PowerShell Core が使っている Json.NET のバージョンと合わせる必要があります。

根底としては PowerShell Core の問題ではなく .NET Core の問題 (AppDomain をサポートしていないことによる) なのですが、こんな時代になっても DLL 地獄に悩まされるのはどうなのかなあと思ってしまいます。

なお GitHub でもだいぶ前から Issue は上がっていますが Open のままとなっています。ちなみにこちらでは、異なるモジュールをインポートしたときにそれぞれが異なるバージョンのアセンブリを使っているとエラーになるよという内容ですが、どちらかというとこちらの問題のほうがエグいですね。

github.com