PowerShell Binary Module を作ってみた

2024/2/26追記

こちらのブログで作成したコードはコマンドツールとしてリメイクしました。

github.com

ちなみに、作成したツールの記事は以下になります。

www.neko3cs.net


最近 PowerShell での自動化がマイブームです。

ちょっとしたツールとかも PowerShell スクリプトで作ったりしますが、あんまり複雑なものだと C# バッチアプリで書いた方が楽じゃないかと思う時があります。

そんな中いろいろ調べてみたら C#PowerShell のコマンドが作れるバイナリモジュールというものを知りました。

なので作ってみます。

作ったもの

作ったものは GitHub に公開しました。

.NET Core のバージョンと入れた Nuget は以下の通りです。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <StartupObject />
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="System.Management.Automation" Version="7.0.0" />
  </ItemGroup>

</Project>

主要なコードは以下になります。

using System;
using System.IO;
using System.Management.Automation;
using System.Security.Cryptography;
using System.Text;

namespace PwshTools.CryptSecretString
{
    [Cmdlet("Crypt", "SecretString")]
    [OutputType(typeof(string))]
    [Alias("crypt")]
    public class CryptSecretStringCommand : Cmdlet
    {
        private TripleDESCryptoSecretInfoRepository _tripleDESCryptoSecretInfoRepository;

        [Parameter(Position = 0, Mandatory = true, ValueFromPipeline = true)]
        public string Value { get; set; }
        [Parameter(Position = 1)]
        public SwitchParameter Decrypt { get; set; }
        [Parameter(Position = 2)]
        public string Key { get; set; }
        [Parameter(Position = 3)]
        public string InitializationVector { get; set; }

        public CryptSecretStringCommand()
            : this(new TripleDESCryptoSecretInfoRepository(new TripleDESCryptoSecretInfoRepositoryConfig())) { }

        public CryptSecretStringCommand(TripleDESCryptoSecretInfoRepository repository)
        {
            _tripleDESCryptoSecretInfoRepository = repository;
        }

        protected override void BeginProcessing()
        {
            if (Key != null && InitializationVector != null) return;

            var info = _tripleDESCryptoSecretInfoRepository.GetInfo();
            if (info != null)
            {
                Key = info.Key;
                InitializationVector = info.IV;
                return;
            }

            info = _tripleDESCryptoSecretInfoRepository.CreateInfo();
            Key = info.Key;
            InitializationVector = info.IV;
        }

        protected override void ProcessRecord()
        {
            if (Decrypt.IsPresent)
            {
                WriteObject(DecryptEncryptedString(Value));
            }
            else
            {
                WriteObject(EncryptPlainString(Value));
            }
        }
        
        // ...(省略)
        
    }
}

公式のドキュメントを見ても理解出来ず、いろんな技術ブログや PowerShell Core そのものの実装とかを参考にして書いたので正しいかは分かりません。

より良い実装があれば教えていただけると嬉しいです。

実装において気をつける点は以下です。

[Cmdlet("Crypt", "SecretString")]

呼び出す際のコマンドレット名は CmdletAttribute で設定します。

第一引数が Verb、第二引数が Noun になります。

通常 Verb は VerbsCommon クラスから取得した方が良いです。

PowerShell のコマンドの命名規則は厳格です。

勝手な命名をすると警告が出てうざいです。

警告は後ほど表示します。

[OutputType(typeof(string))]

おそらく戻り値の型を指定するものだと思います。

今回は文字列が型なのであまり効果を示さない気がしますが、ユーザークラス等の独自の型の場合はパイプライン処理等をする際に活きるものな気がします(推測)。

[Alias("crypt")]

コマンドのデフォルトエイリアスを指定出来ます。

なくてもいい気がします。

public class CryptSecretStringCommand : Cmdlet

PowerShell コマンドにするには Cmdlet クラスを継承します。

他にも PSCmdlet クラスなるものがあるらしいですが、Runspace 上でしか実行できないためユニットテストをしたい場合などにちょっと苦労します。*1

[Parameter(Position = 0, Mandatory = true, ValueFromPipeline = true)]

コマンドの引数はプロパティで実装し、 ParameterAttribute を使って引数であることを指定します。

PowerShell スクリプトでも使うことはありますが詳細は公式ドキュメント*2を参考にしてください。

protected override void BeginProcessing()

コマンドの処理が実行される前の準備処理があればここに実装します。

この他に EndProcessing() というメソッドもあり、同様にコマンドが実行された後の始末処理をします。

protected override void ProcessRecord()

実際のコマンドの処理はここに記述します。

サンプルではプライベートメソッドを呼び出していますが、ここに書いた処理がコマンドを実行した際に呼び出されるという点以外は普通のクラスと同じです。

なので、責務分割のためにクラス分けするもよしですし、このメソッドで全処理書いてしまってもよしです。

作ったらリリースビルドしてマニフェストファイルを作成する

実装が終わったらリリースビルドをして PwshTools.CryptSecretString.dll ファイルを回収します。

リリースビルドした結果

この .dll ファイルがコマンドの実態になります。

ですが、このファイルだけではコマンドとして使用することは出来ません。

マニフェストファイルという物が必要です。

でも、これも以下のコマンドで生成出来るので心配ありません。

PS> New-ModuleManifest -Path .\PwshTools.CryptSecretString.psd1 -RootModule "PwshTools.CryptSecretString.dll" -Author "neko3cs"

上記のコマンドを実行すると PwshTools.CryptSecretString.psd1 が生成されます。

どこでもコマンドが使えるようにプロファイルに登録しておく

作ったコマンドを使えるように登録するには Import-Module コマンドを使用します。

ですが、Import-Module が有効になるのは呼び出されたターミナルでのみです。

なので、PowerShell のプロファイルに記載しておくことで PowerShell を起動する度に初期処理で登録されるようにします。

PowerShell のプロファイルは Microsoft.PowerShell_profile.ps1 という名前のファイルです。

$PROFILE 自動変数にパスが入っています。

記載したコードは以下になります。

$MyPSModules = (
    'C:\Repos\pwsh-tools\modules\PwshTools.CryptSecretString\'
)
foreach ($module in $MyPSModules) {
    if (Test-Path $module) {
        Import-Module $module
    }
}

Import-Module する際は*.psd1*.dll のあるディレクトリパスを指定します。

あと、後からモジュールを追加した時のために foreach にかけています。

これで次 PowerShell が起動したタイミングから作った PowerShell コマンドが使えるようになります。

作ったコマンドを使ってみる。

実行結果は以下の通りです。

Crypt-SecretString 実行

きちんとコマンドの引数も Tab 補完で表示されます。

ただ、前節で言った通り Verb が命名規則に従われていないという警告が出ています。

本来は従うべきだとは思いますが、面倒臭いので無視します。 *3

まとめ

C# を用いて PowerShell のコマンドを自作してみました。

作ってみた感想は以下の通りです。

  • 使い捨てのツールの場合は C# バッチアプリで十分
    • Import-Module が少し手間に感じる
  • 何度もよく使うツールの場合はターミナルが開かれていればすぐに使えるので便利
    • C# バッチアプリだと .exe ファイルの場所まで移動しないと使えない

私自身、学生時代に Unix で鍛えられたためか CLI の方が馴染みがあります。

なので、何度も使うもので PowerShell スクリプトで作るには少し複雑かなと思うものはバイナリモジュールで実装してもいいかなと思いました。

*1:C# で書いた PSCmdlet のテスト - あえとすさん

*2:ParameterAttribute Class - MS Docs

*3:気が変わったら Get-CryptedString とか PowerShell命名規則に合わせるかもしれません。