rksoftware

Visual Studio とか C# とかが好きです

PowerShell のコマンドを作る

PowerShell は自分でコマンドを作れます。少なくとも C# で。他の方法で作れるのかは未確認です。

■ コード

まずはコードから。こんな感じです。
.NET Standard のクラスライブラリです。今回は PS というプロジェクトを作りました。

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="PowerShellStandard.Library" Version="5.1.1" />
  </ItemGroup>
</Project>
namespace PS
{
    [System.Management.Automation.Cmdlet(System.Management.Automation.VerbsCommunications.Send, "Bottoms")]
    public class PerfectSoldier : System.Management.Automation.Cmdlet
    {
        protected override void BeginProcessing() => WriteVerbose("Begin!");
        protected override void ProcessRecord() => WriteVerbose("Processing!");
        protected override void EndProcessing() => WriteVerbose("End!");
    }
}

プロジェクトの作り方は次のコマンドでプロジェクトを作って、nuget からパッケージを入れただけです。

dotnet new classlib -n PS -f netstandard2.0
cd .\PS
dotnet add package PowerShellStandard.Library

コードとしては System.Management.Automation.Cmdlet クラスを継承したクラスに System.Management.Automation.Cmdlet 属性を付けているだけです。
属性の第一引数と、第二引数をつなげたものがコマンド名になるようです。今回は Send + Bottoms = Send-Bottoms
クラスは internal でなく public にしてください。

■ コマンド登録

作ったコマンドは登録しないと使えません。PS.dll は前述のコードで作った dll です。

Import-Module PS.dll

■ コマンド実行

Send-Bottoms

これで実行できます。今回は WriteVerbose しているので、-Verbose を付けると出力があります。

Send-Bottoms -Verbose
詳細: Begin!
詳細: Processing!
詳細: End!

■ コマンド削除

作ったコマンドをいつまでも残しておくと邪魔になることがあります。試しに作ったものであれば消しておきましょう。

Remove-Module PS

PS という名前は次のようにして調べられます。

Get-Module

ModuleType Version    Name                                ExportedCommands
---------- -------    ----                                ----------------
Binary     1.0.0.0    PS                                  Send-Bottoms

■ 簡単ですね

簡単ですね。

難しかった

嘘です。簡単じゃないです。ここまでたどり着くのに苦労しました。Learn のドキュメント、ページはあるのに恐ろしい文章量の少なさ。全然情報がありませんでした。

■ 難しい話の前に

難しい話の前にコードを見たらだれでも気になるだろうこれについて。名前の通り、開始時の処理、処理本体、終了時の処理を書けばよいのでしょう。とすれば、BeginProcessing でリソースを確保して、 EndProcessing で開放すればいい感じに動いてくれる、そうなってくれたらうれしいと思いつつも、信じられない。読んでくれているあなたもそう思っていますね?

  • BeginProcessing()
  • ProcessRecord()
  • EndProcessing()

試してみましょう。

protected override void BeginProcessing() => throw new Exception("test");
protected override void ProcessRecord() => WriteVerbose("Processing!");
protected override void EndProcessing() => WriteVerbose("End!");
Send-Bottoms -Verbose
Send-Bottoms : test
発生場所 行:1 文字:1
+ Send-Bottoms -Verbose
+ ~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [Send-Bottoms], Exception
    + FullyQualifiedErrorId : System.Exception,PS.PerfectSoldier

ダメでした。しかしもしかしたら、BeginProcessing だからかもしてません。ProcessRecord ならきっと!

protected override void BeginProcessing() => WriteVerbose("Begin!");
protected override void ProcessRecord() => throw new Exception("Processing");
protected override void EndProcessing() => WriteVerbose("End!");
Send-Bottoms -Verbose
詳細: Begin!
Send-Bottoms : Processing
発生場所 行:1 文字:1
+ Send-Bottoms -Verbose
+ ~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [Send-Bottoms], Exception
    + FullyQualifiedErrorId : System.Exception,PS.PerfectSoldier

ダメでした。無念。
あきらめきれないので、エラー時と終了時どちらも動く場所があるのではないか? System.Management.Automation.Cmdlet クラスを見てみましょう。

public abstract class Cmdlet : InternalCommand
{
    public ICommandRuntime CommandRuntime { get; set; } }
    public static HashSet<string> CommonParameters;
    public static HashSet<string> OptionalCommonParameters;
    public bool Stopping;
    protected virtual void BeginProcessing() { }
    protected virtual void EndProcessing() { }
    public virtual string GetResourceString(string baseName, string resourceId) { }
    public IEnumerable Invoke() { }
    public IEnumerable<T> Invoke<T>() { }
    protected virtual void ProcessRecord() { }
    public bool ShouldContinue(string query, string caption, bool hasSecurityImpact, ref bool yesToAll, ref bool noToAll) { }
    public bool ShouldContinue(string query, string caption, ref bool yesToAll, ref bool noToAll) { }
    public bool ShouldContinue(string query, string caption) { }
    public bool ShouldProcess(string verboseDescription, string verboseWarning, string caption, out ShouldProcessReason shouldProcessReason) { }
    public bool ShouldProcess(string target) { }
    public bool ShouldProcess(string target, string action) { }
    public bool ShouldProcess(string verboseDescription, string verboseWarning, string caption) { }
    protected virtual void StopProcessing() { }
    public void ThrowTerminatingError(ErrorRecord errorRecord) { }
    public bool TransactionAvailable() { }
    public void WriteCommandDetail(string text) { }
    public void WriteDebug(string text) { }
    public void WriteError(ErrorRecord errorRecord) { }
    public void WriteInformation(object messageData, string[] tags) { }
    public void WriteInformation(InformationRecord informationRecord) { }
    public void WriteObject(object sendToPipeline, bool enumerateCollection) { }
    public void WriteObject(object sendToPipeline) { }
    public void WriteProgress(ProgressRecord progressRecord) { }
    public void WriteVerbose(string text) { }
    public void WriteWarning(string text) { }
}

virtual メソッドは他には StopProcessing ですね。試してみましょう。

namespace PS
{
    [System.Management.Automation.Cmdlet(System.Management.Automation.VerbsCommunications.Send, "Bottoms")]
    public class PerfectSoldier : System.Management.Automation.Cmdlet
    {
        protected override void BeginProcessing() => WriteVerbose("Begin!");
        protected override void ProcessRecord() => ThrowTerminatingError(new System.Management.Automation.ErrorRecord(new Exception("Processing"), "Error ID", System.Management.Automation.ErrorCategory.InvalidData, null));
        protected override void StopProcessing() => WriteVerbose("Stop!");
        protected override void EndProcessing() => WriteVerbose("End!");
    }
}
Send-Bottoms -Verbose
詳細: Begin!
Send-Bottoms : Processing
発生場所 行:1 文字:1
+ Send-Bottoms -Verbose
+ ~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidData: (:) [Send-Bottoms]、Exception
    + FullyQualifiedErrorId : Error ID,PS.PerfectSoldier

ダメでした。無念。

難しい話

情報としてこの辺りが検索でヒットします。 それを参考にライブラリのドキュメントも見ます。 リンク先もたどっても全然わからない......

と絶望感の中、試行錯誤しつつネットの海を漂っていたら見つけました。

テンプレートをインストールして

dotnet new -i Microsoft.PowerShell.Standard.Module.Template

プロジェクトを作ります。

dotnet new psmodule -n PS

できたプロジェクトにはこんなコードが入っていました。

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

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <AssemblyName>PS</AssemblyName>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="PowerShellStandard.Library" Version="5.1.0-preview-06">
      <PrivateAssets>All</PrivateAssets>
    </PackageReference>
  </ItemGroup>

</Project>
using System;
using System.Management.Automation;
using System.Management.Automation.Runspaces;

namespace PS
{
    [Cmdlet(VerbsDiagnostic.Test,"SampleCmdlet")]
    [OutputType(typeof(FavoriteStuff))]
    public class TestSampleCmdletCommand : PSCmdlet
    {
        [Parameter(
            Mandatory = true,
            Position = 0,
            ValueFromPipeline = true,
            ValueFromPipelineByPropertyName = true)]
        public int FavoriteNumber { get; set; }

        [Parameter(
            Position = 1,
            ValueFromPipelineByPropertyName = true)]
        [ValidateSet("Cat", "Dog", "Horse")]
        public string FavoritePet { get; set; } = "Dog";

        // This method gets called once for each cmdlet in the pipeline when the pipeline starts executing
        protected override void BeginProcessing()
        {
            WriteVerbose("Begin!");
        }

        // This method will be called for each input received from the pipeline to this cmdlet; if no input is received, this method is not called
        protected override void ProcessRecord()
        {
            WriteObject(new FavoriteStuff { 
                FavoriteNumber = FavoriteNumber,
                FavoritePet = FavoritePet
            });
        }

        // This method will be called once at the end of pipeline execution; if no input is received, this method is not called
        protected override void EndProcessing()
        {
            WriteVerbose("End!");
        }
    }

    public class FavoriteStuff
    {
        public int FavoriteNumber { get; set; }
        public string FavoritePet { get; set; }
    }
}

これを参考にあとはセンスで頑張ります。頑張った結果が冒頭です。

PSCmdlet vs Cmdlet

コマンドを作る際に、基底クラスにするのは PSCmdletCmdlet の二つの選択肢があるそうです。その違いは、PSCmdlet はコマンドが呼ばれた環境を見られるという事らしいです。
そういったことの必要ない、コマンドの引数だけを条件に動作するコマンドなら Cmdlet でよさそう。私にはまだよくわからないので、良くわからないという事はしばらく Cmdlet で生きて行けそうです。

ひとまず、PSCmdlet の定義を見てみましょう。

public abstract class PSCmdlet : Cmdlet
{
    public PSEventManager Events;
    public PSHost Host;
    public CommandInvocationIntrinsics InvokeCommand;
    public ProviderIntrinsics InvokeProvider;
    public JobManager JobManager;
    public JobRepository JobRepository;
    public InvocationInfo MyInvocation;
    public PagingParameters PagingParameters;
    public string ParameterSetName;
    public SessionState SessionState;
    public PathInfo CurrentProviderLocation(string providerId) { return null; }
    public Collection<string> GetResolvedProviderPathFromPSPath(string path, out ProviderInfo provider) { }
    public string GetUnresolvedProviderPathFromPSPath(string path) { }
    public object GetVariableValue(string name) { }
    public object GetVariableValue(string name, object defaultValue) { }
}

取り敢えず扱いやすそうな、Host プロパティでやってみます。

namespace PS
{
    [System.Management.Automation.Cmdlet(System.Management.Automation.VerbsCommunications.Send, "Bottoms")]
    public class PerfectSoldier : System.Management.Automation.PSCmdlet
    {
        protected override void BeginProcessing() => WriteVerbose("Begin!");
        protected override void ProcessRecord() => WriteVerbose($"Begin! {this.Host.CurrentCulture}");
        protected override void StopProcessing() => WriteVerbose("Stop!");
        protected override void EndProcessing() => WriteVerbose("End!");
    }
}
Send-Bottoms -Verbose
詳細: Begin!
詳細: Begin! ja-JP
詳細: End!
Set-Culture -CultureInfo en-GB

環境を開きなおして

Send-Bottoms -Verbose
詳細: Begin!
詳細: Begin! en-GB
詳細: End!

なるほど。これはこういうことなのですね。

■ 難しいですね

PS、難しいですね。