2022-05-07

F#で PowerShell コマンドレットを書き始めた

勉強がてら、F# で PowerShell のコマンドレットを書きはじめる。 ↓ を参考にやる。ただし .NET 6.0 を対象にする。

Writing a PowerShell Core Module With F#, A Complete Guide | Brianary

初めての要素が多いので、ゆっくり進める。

  • F# でアプリ書くのが初
  • コマンドレットを書くのが初
  • dotnet CLI を使った開発が初
  • その他 PowerShell 系のツール(Pester,platyPS)をちゃんと使うのが初
    • 参考にする記事にはないが、ビルドスクリプトも psake で書く

題材としては、poco の再実装をしてみるつもり。

諸々のツールが出てくるので一覧しておく。

repo とプロジェクトを作成する

動くものになるまでは private repo で運用する。動かないなら公開しても意味がないからね。

# private repo を作成する。
$owner = 'krymtkts'
$module = 'pocof'
New-GitHubRepository -RepositoryName 'pocof' -Private

# remote repo を clone する。
ghq get -p (Get-GitHubRepository -OwnerName $owner -RepositoryName $module | Select-Object -ExpandProperty ssh_url)
cd "$(ghq root)/$(ghq list $module)"

# initial commit を刻む。
gpg-connect-agent reloadagent /bye # <- gpg-agent が立ち上がってこないので先に起こす。
git commit --allow-empty -m 'Initial commit.'
dotnet new sln

# 空のプロジェクトを作成。
dotnet new classlib --language 'F#' --framework net6.0 -o src/$module
dotnet sln add src/$module/$module.fsproj
cd src/$module
# PowerShell 開発の依存関係を追加。
dotnet add package PowerShellStandard.Library
New-ModuleManifest "$module.psd1"
'<helpItems schema="maml" xmlns="http://msh" />' | Set-Content "$module.dll-Help.xml" -Encoding utf8

これで repo と空の F# プロジェクト pocof.fsproj ができた。 次に、記事に記載の通り、 pocof.fsproj に必要な情報を加筆する。

  • PropertyGroupVersion 属性を追加
  • ItemGroup 属性を追加して、コピーするファイルを記載する

また PowerShell のモジュール情報を pocof.psd1 書き込む。 コマンドレットなので、 RootModule を書き忘れるとコマンドがインポートできないのでご注意(書き忘れてて Import-Module しても使えない!となった)。

あと作成後に気づいたのだが、 作成された *.sln 等のエンコーディングが UTF8 with BOM だったり改行が CRLF だったりするので、それらを UTF8 と LF に手で補正した。

最後に 空の XML-based help file を作成しているが、これは Get-Help で使うヘルプファイルで、後で PlatyPS で上書きされるやつ。 Naming Help Files - PowerShell | Microsoft Docs

F# 開発環境を準備する

わたしは VS Code を使っているので、F# 用の拡張機能を入れる。

F# でフォーマッタといえば fantomas のようなのだけど、個別に入れなくても Ionide がうまく拾えるらしい。 Formatting Settings · Issue #1346 · ionide/ionide-vscode-fsharp

コードを編集する

自動生成されたコードを編集する。 記事に記載のものだとパラメータに Position がなくて Pester こけそうなのと、自分のやりたいことに合わせた引数を書くなど諸々の調整をする。

Pester のテストコードも作成する。テストコードは tests ディレクトリ配下に配置した。

ビルド&テスト実行

記事に記載の通りだと typo があったり Import-Module 前に削除があって消えてしまう等ある。 編集して実行した。

$ModuleName = Resolve-Path ./src/* | Split-Path -Leaf
Import-LocalizedData -BindingVariable module -BaseDirectory (Resolve-Path ./src/*) -FileName "$ModuleName.psd1"
if ($module.ModuleVersion -ne (Resolve-Path ./src/*/*.fsproj | Select-Xml '//Version/text()').Node.Value) {
throw 'Module manifest (.psd1) version does not match project (.fsproj) version.'
}
dotnet publish
cp (Resolve-Path ./src/*/bin/Debug/*/publish/FSharp.Core.dll) (Resolve-Path ./src/*/bin/Debug/*/) -Verbose

# import して Pester 実行。
# モジュールのバージョンであったり export される Cmdlet が正しいかなど見る。
Import-Module (Resolve-Path ./src/*/bin/Debug/*/publish/*.psd1) -Force
Invoke-Pester

あとこれらを psake で実行できるように psakefile.ps1 に記載した。

F# のプロジェクトだったら FAKE を使った方がいいのかなーと考えていた。となると、依存する Paket も入れないといけない。いま dotnet CLI で管理してたのでいらないかなーと思った。 また、今回作りたいのは PowerShell のコマンドレットなので、 周辺のツールも PowerShell だ。これをいちいち fake.cmddotnet fake buildを介して実行するのなんかダルいな...と億劫だったので、 FAKE の導入は見送ることにした。

ドキュメントを生成する

記事では platyPS を使っているのでそれに従う。

# 事前にビルドしたモジュールを Import-Module しておく。
New-MarkdownHelp -Module pocof -OutputFolder ./docs -ErrorAction SilentlyContinue
# dll-Help.xml を作るときに実行する。
New-ExternalHelp docs -OutputPath (Resolve-Path ./src/*/) -Force

コメントに記した通り、他のタスクと依存関係にある。こういうのは psakefile.ps1 にまとめる。

ヘルプの作成元は Markdown をオリジナルとする方針。

PowerShell Gallery へのリリース

まだやらないけど手順だけ確立しておく。

記事に記載のものだと、ビルドコンフィグレーションを Release にして Import、その後 Import したモジュールを公開、という手順になっている。

個人的にやったことがあるのはフォルダを指定して公開する方だけなので、どっちにするかなーと悩んでいる。間違って古い Import 済みモジュールを公開するミスとかないのかな、というのが疑問。

ま、勉強のためのものであるし、やったことない方を接客的に選んでみるのも一興か。コマンドレットが正しくエクスポートできているか、リリース前にテストするにもこの方法を取るのが良さそうだし。

とはいえ記事に記載の通りやると、自分が普段利用している PowerShell Module のパスに直でリリース前のモジュールを打ち込んでしまう。 これはちょっと実用始めたら困りそうなので、ディレクトリを変える必要がある。 となると結局いつものパス指定でのリリース方式でええんちゃうか...

いずれを選択するかはまた検討したい。

また、これらのリリースタスクも psakefile.ps1 に定義するものとする。

おわりに

これでひとまず準備完了。 ほぼ記事に記載のとおりやってきたけど、ちょいちょい自分用に変えたり、新しい要素については色々調べながらやってるので、進捗は亀のスピードだった。 動くものをこしらえれたら public repo にしよう。

現時点で不足しているとわかっている点もある。 今回書きたいコマンドレットはインタラクティブなものなので、きっと Pester でテストできない点が出てくる。 それらは FsUnit を使って可能な限りテストを書きたい所存。

あと困っているのが、参考にしている記事にも記載があったが、 DLL への参照が切れなくてファイルを消したりできなくなる(Remove-Module 忘れとかでなく)。いまは都度 PowerShell のセッションを作り直してるけど相当に面倒なので、原因を突き止めてどうにかしたいな。

未経験のものに触れるのは普段得られない刺激があって良い。