tech.guitarrapc.cóm

C#, PowerShell, Unity, Cloud, Serverless Technical Update and Features

PowerShell のモジュール詳解とモジュールへのコマンドレット配置手法を考える

PowerShell Advent Calendar 2013 に参加させていただいています。これは2日目の記事となります。

今回は、Windows PowerShell のモジュール機構を利用するにあたり以下の2つに関して考えてみようと思います。

  • 4つあるモジュール各種の詳解
  • モジュールへのコマンドレット配置手法

注意 :

本記事はPowerShell 3.0以上 (私の環境は PowerShell 4.0)をベースとしています。PowerShell 2.0 では一部パラメータ名称が異なったりしますが、そこは察し(

推奨 :

以前書いた Windows 8.1 や Windows Server 2012 R2 以外で Windows PowerShell 4.0を利用する方法を参考にしていただければ幸いです。

目次

なぜモジュールを利用するのか

とあるPowerShellerの思い

少し PowerShell を触るとわかりますが、 PowerShellコードは再利用性を持って記述することが容易です。そのため自作のファンクションをサクサク書いて利用するようになると、PowerShell.exe や PowerShell_Ise.exe を起動したときに自動的に読み込まれてほしいと思うようになります。

毎回書くとか人間のすることではありません。

簡易的な自動的なfunction読み込み : $Profile

簡易的なファンクション自動読み込み方法として、 $Profile にファンクションを記述するやり方があります。$Profileは、PowerShell.exe や PowerShell_Ise.exe 起動時に自動的に読み込まれるため、$Profileに記述したファンクションも自動的に実行され任意のタイミングで利用できるようになります。

$Profileの限界とモジュール機構

せっかく作った一連のPowerShell Scriptを$Profile に書き連ねるのは限界があります。 例えば、 自作/他の人が書いたファンクションを50個も$profileに書いておいたとして、何が含まれるか管理できるでしょうか?あるいは、読み込ませたくないものを制御できるでしょうか?

いいえ、$profile では例え Get-Command を用いても どのファンクションが$profileに含まれたものなのか判断が困難であり、自作コマンドレットが秩序なく散らばる要因となりねません。この問題に対して、PowerShell 2.0からはモジュール機構が実装されました。

モジュールは何をできるの

モジュールは、HDDやSSDといった記憶媒体に保持した PowerShell ファンクション/スクリプト の参照、読み込み、保持機構です。スナップイン(snap-in)とは異なりモジュールは、メンバーとして コマンドレット、プロバイダ、ファンクション、変数、エイリアスなどを持ることができます。

モジュール機構を利用することで、一連のスクリプトやファンクションをモジュールとしてパッケージ化、読み込みできるようになります。特にPowerShell3.0 からはモジュールに含まれるコマンドレットは自動的に探索され、インテリセンスでも候補に挙がります。さらにそのコマンドレットを実行した時に、コマンドレットを含むモジュールも自動的に読み込まれます。

もちろん Get-Command でもModuleNameプロパティでどのモジュールにそのファンクションが含まれるのか、あるいはそのモジュールに含まれるファンクションが何かを判断できます。


Windows PowerShell Module Concepts

Windows PowerShell においてモジュールのコンセプトはタイプごとに4つあります。

モジュールタイプ 概要
Script Modules スクリプトファイル(.psm1) に含まれたPowerShell コードを指します。開発者や管理者はモジュールメンバーのファンクションや変数などを利用できます。
Binary Modules .NET Framework アセンブリ(.dll)に含まれたコンパイルされたコードを指します。開発者はこのモジュールにCmdletやプロバイダ、あるいは既存のSnap-inも利用できます。
Manifest Modules コンポーネントをマニフェスト(.psd1)で定義し、単一、あるいは複数のルートモジュールファイル(.psm1)読み込みをマニフェストで記述制御でき、アセンブリ、型、フォーマットなどを読み込めます
Dynamic Modules ディスク(ファイル)として保持されていないものを指します。オンデマンドに、ディスク保持する必要がないモジュールを可能にし、New-Module で作成、利用できます。

それぞれを細かく見ていきましょう。


I. Script Module

簡単にいうと、モジュールファイル(.psm1) で各種 function/Workflow/filter/変数/エイリアス を定義したものを指します。サンプル を PowerShell_ise.exe でコマンド実行しながら考えましょう。

1. モジュールパスにモジュールフォルダを作成する

まず、ユーザーモジュールパス($env:USERPROFILE\Documents\WindowsPowerShell\Modules)にhogeフォルダを作ります。

mkdir $env:USERPROFILE\Documents\WindowsPowerShell\Modules\hoge
  • ユーザーモジュールパスについては後段で説明します。

2. モジュールフォルダにモジュールファイル(.psm1)を作成する

次にモジュールファイルである hoge.psm1をモジュールフォルダ直下に作成します。

モジュールファイル(.psm1)は必ずモジュールフォルダと同一名称にしてください。

function write-hosthoge
{
    Write-Host "hoge"
}

function Not-ExportFunction
{
    "出力しないもん!"
}

Export-ModuleMember -Function write-hosthoge
.psm1記述内容について

ここでやっていることはいたって簡単です。

write-hosthoge というfunctionを定義して、Export-ModuleMember コマンドレットで write-hosthogeファンクションのみ 「モジュールの公開するファンクション」として出力しています。

Export-ModuleMember による公開するファンクションの選択

このExport-ModuleMemberですが、「公開するfunction = public を指定している」 と考えれば性質がわかりやすいかもしれません。

  • private のイメージ : Export-ModuleMember で出力せずとも、.psm1 内部のfunction同士では参照しあえます。(例えスクリプトファイル(.ps1)ごとに分離させてもドットソース読み込みしていれば同様です。)
  • public のイメージ : Export-ModuleMember で出力することで、モジュール利用者が自由にコマンドレットを実行することができます。

つまり、モジュール内部で他のプログラミング言語のように用途に応じて細かくファンクションを作っても、外部に公開するファンクション や 変数は選べるということです。

通常のスクリプトファイル(.ps1) で 特定のファンクションを公開したくない場合、function{}内部にそのfunction{}をネストして記述するなどして隠す必要がありますが、モジュールではExport-ModuleMemberで制御できます。

  • すべてのファンクションを公開する場合は、 ワイルドカード '*' を指定します。
Export-ModuleMember -Function *

3. モジュールメンバーの実行 と 暗黙的なモジュールの読み込み

モジュールフォルダとモジュールファイル(.psm1)を同一名で作成を終えたら、PowerShell.exe か PowerShell_Ise.exe を起動して次のコマンドを入力してみてください。

write-hosthoge

結果、hogeと出力されます。つまり、hogeモジュールが自動的に読み込まれwrite-hosthogeコマンドレットが実行されているのです。

PS> write-hosthoge
hoge

PowerShell 3.0以降は、モジュールメンバーは明示的なモジュールファイル読み込みをすることなく実行可能です。

4. モジュール読み込みの確認

モジュールの読み込み候補

PowerShell3.0 以降において、モジュールに対して暗黙的な読み込みが実装されました。

これにより、PowerShellでインテリセンス実行時に 自動的に 環境変数$Env:PSModulePathを探索し、正しくモジュールと解釈できるモジュールのファンクションを読み込みます。

デフォルトの環境変数は以下の状態です。

C:\Users\UserName\Documents\WindowsPowerShell\Modules;C:\Program Files\WindowsPowerShell\Modules;C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules\

以下の3つのパスです。

C:\Users\UserName\Documents\WindowsPowerShell\Modules;
C:\Program Files\WindowsPowerShell\Modules;
C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules\;

これらのパスに存在するモジュールは、明示的にモジュールを読み込ませずともインテリセンスの候補に上がり、コマンド実行時にImport-Moduleがバックグランドで実行されモジュールが読み込まれます。

システムレベルとユーザーレベルのモジュールパス

モジュールパスは、システムレベルとユーザーレベルでパスを保持しています。

システムレベルとユーザ-レベルでは大きく挙動が変わります。

システムレベルパス

特に以下が該当します。

C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules\;

また、x64環境の場合、 PowerShell x86 は、以下のパスがシステムレベルパスになります。

C:\Windows\SysWOW64\WindowsPowerShell\v1.0\Modules

少し見てみるとPowerShellの既定のモジュールはすべてここに含まれていることがわかります。

PS> ls C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules\ | fw


    ディレクトリ: C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules



[AppBackgroundTask]                                         [AppLocker]
[Appx]                                                      [AssignedAccess]
[BitLocker]                                                 [BitsTransfer]
[BranchCache]                                               [CimCmdlets]
[Defender]                                                  [DirectAccessClientComponents]
[Dism]                                                      [DnsClient]
[Hyper-V]                                                   [International]
[iSCSI]                                                     [ISE]
[Kds]                                                       [Microsoft.PowerShell.Diagnostics]
[Microsoft.PowerShell.Host]                                 [Microsoft.PowerShell.Management]
[Microsoft.PowerShell.Security]                             [Microsoft.PowerShell.Utility]
[Microsoft.WSMan.Management]                                [MMAgent]
[MsDtc]                                                     [MSOnline]
[MSOnlineExtended]                                          [NetAdapter]
[NetConnection]                                             [NetEventPacketCapture]
[NetLbfo]                                                   [NetNat]
[NetQos]                                                    [NetSecurity]
[NetSwitchTeam]                                             [NetTCPIP]
[NetWNV]                                                    [NetworkConnectivityStatus]
[NetworkTransition]                                         [PcsvDevice]
[PKI]                                                       [PrintManagement]
[PSDesiredStateConfiguration]                               [PSDiagnostics]
[PSScheduledJob]                                            [PSWorkflow]
[PSWorkflowUtility]                                         [ScheduledTasks]
[SecureBoot]                                                [SmbShare]
[SmbWitness]                                                [StartScreen]
[Storage]                                                   [TLS]
[TroubleshootingPack]                                       [TrustedPlatformModule]
[VpnClient]                                                 [Wdac]
[WindowsDeveloperLicense]                                   [WindowsErrorReporting]
[WindowsSearch]

これらのモジュールは、ユーザーモジュールパスと異なり、どのユーザーであってもPowerShellエンジンの起動時に自動的にImport-Moduleされています。

つまり、ローカルの PowerShell.exe を起動時はインポートされていませんが、Invoke-Command -ComputerNameNew-PSSessionなどでWinRM経由で接続したときには、接続時にはこれらのモジュールが読み込まれています。

特にリモートセッションで特定のユーザーごとにモジュールを持たせるのが著しく手間であり、信用できるスクリプトの場合は大きく手間が省けることになります。

ユーザーレベルパス

以下が該当します。が、ドキュメントフォルダを移動している場合は、その限りではありません。

$env:USERPROFILE\Documents\WindowsPowerShell\Modules

ユーザーレベルパスは、現在のユーザーのPowerShellセッションでしか参照されません。

そのため、 ユーザー hoge さんで接続しているときに、ユーザーfugaさんのユーザーモジュールパスが参照されることはありません。

これは、ユーザーごとのモジュール選択可能性を可能にしており、PSリモーティングでも接続に利用したユーザーのユーザーフォルダしか参照されることはありません。

明示的なモジュールの読み込み

前述の通り、PowerShell 3.0以降は明示てきなモジュールの読み込みことなく暗黙的にモジュールの探索が行われ、コマンドレット実行時に モジュールの読み込みが自動的になされます。

ただ開発中の場合は、モジュールの読み込みが確認できれば、Export-ModuleMember で出力したメンバーを確認することもできるでしょう。 もし、コマンドレット実行前にモジュールを事前に読み込みたい、あるいはPowerShell 2.0を使っている場合は、明示的にモジュールを読み込んでみましょう。

Import-Module hoge -PassThru

ModuleType を見ることで、psm1読み込みによる Script Module であることがわかります。

PS> Import-Module hoge -PassThru

ModuleType Version    Name                                ExportedCommands
---------- -------    ----                                ----------------
Script     0.0        hoge                                write-hosthoge

これでhogeモジュールが明示的に読み込まれ、write-hosthogeファンクションやモジュールに含まれる変数が読み込まれます。

もしモジュール読み込み時に、読み込まれたモジュール結果出力がいらない場合は、-PassThruスイッチを外します。

Import-Module hoge

このように読み込まれたモジュール結果が出力されません。

PS> Import-Module hoge -PassThru
PS>
読み込まれるモジュールメンバーの確認

Import-Module 時に、-Verboseスイッチを付けることでモジュール読み込み時にインポートされたファンクション/変数が確認できます。

Import-Module hoge -PassThru -Verbose
PS> Import-Module hoge -PassThru -Verbose
VERBOSE: Importing function 'write-hosthoge'.

ModuleType Version    Name                                ExportedCommands
---------- -------    ----                                ----------------
Script     0.0        hoge                                write-hosthoge
読み込まれたモジュールの確認

現在の PowerShellプロセスで実行されている RunSpaceに読み込まれたモジュールを確認するには以下のようにします。

Get-Module

ここでモジュール名を指定することで、そのモジュールが存在するかわかります。

Get-Module hoge

あるいは、エラーを出さずに判定するなら Where-Objectコマンドレットを使います。

Get-Module | Where Name -eq "hoge"     # PowerShell 3.0のWhere-Object 省略記法
(Get-Module).Where{$_.Name -eq "hoge"} # PowerShell 4.0のWhereメソッド
読み込まれたモジュールメンバーの確認

一度モジュールが読み込まれた後に、PowerShellコマンド全体から指定したモジュールメンバーのコマンドレットを取得することもできます。

Get-Command -Module hoge

今回の場合は、以下のように出力されます。

PS> Get-Command -Module hoge

CommandType     Name                                               ModuleName
-----------     ----                                               ----------
Function        write-hosthoge                                     hoge

5. モジュールの破棄と再読み込み

読み込んだモジュールの破棄

一度読み込んだモジュールをセッションから取り除くなら 以下のようにします。

Remove-Module hoge
一度モジュールを読み込んだセッションでモジュールファイルを変更して読み込み直す

PowerShellは、その起動ごとにRunSpace内で変数やファンクション、読み込んだモジュールなど 各セッション情報を保持しています。つまり、PowerShellを起動しなおすことで新しいRunSpaceでセッションが実行されます。

一方で、一度読み込んだモジュールを現在のRunSpaceを維持したままImport-Moduleしても読み込んだモジュールのまま維持してしまい、新たなモジュール設定を読み込み直すことはありません。この場合、一度読み込んだモジュールを破棄して再度モジュールを読み込み直すか、新しいRunSpaceでモジュールを読み込む必要があります。

PowerShell_Ise.exe の場合は、Ctrl+T で新たなRunSpaceタブを作ることで、iseを閉じることなく新しいセッションを試すことができます。

もし同一RunSpaceでモジュールの再読み込みを行うには、-Forceスイッチを付けてImport-Moduleします。

Import-Module hoge -Force

-Verboseスイッチを付けることで、一度現在のRunSpaceから一度ファンクションが取り除かれてから再度読み込まれていることがわかります。

PS> Import-Module hoge -PassThru -Verbose -Force
VERBOSE: Removing the imported "write-hosthoge" function.
VERBOSE: Loading module from path 'C:\Users\acquire\Documents\WindowsPowerShell\Modules\hoge\hoge.psm1'.
VERBOSE: Importing function 'write-hosthoge'.

ModuleType Version    Name                                ExportedCommands
---------- -------    ----                                ----------------
Script     0.0        hoge                                write-hosthoge

ここまでが、スクリプトモジュールのおおよその概要になります。

続いてバイナリモジュールに関して見てみましょう。


II. Binary Modules

Binary Moduleは、C# で PowerShell アセンブリ(.dll) 形式を生成し、PowerShellにモジュールとして読み込ませる方式を指します。

PowerShell V1 の頃に現在のような「高度な関数」といわれるPowerShellスクリプト単独での細かな制御をファンクションが持ちえなかった頃や、C# の強力な機能を利用する際に作成します。

Microsoft MVP for PowerShellの 牟田口大介さんがバイナリモジュールを用いたモジュールに関するセッションを2013/5/11 のCommunity Open Day 2013 にて行われています。

デモが中心だったため、資料の一つとして紹介にとどめますが、非常に参考になるセッションでした。

運用自動化に役立つPowerShellモジュールの作成方法

さて、バイナリモジュールにおける .dll 生成と モジュール読み込みまでを見てみましょう。

1. Visual Studioで.dllを生成する

PowerShellのバイナリモジュールは C#/VB でコーディング可能です。書くなら断然 C# が楽です。

ということで、みなさん大好き Visual Studioで開発する流れを順を追ってみてみましょう。 *1

1.1 新規プロジェクトと参照の追加

新規プロジェクトで、C# > クラスライブラリを選択します。

参照に 参照 > C:\Program Files (x86)\Reference Assemblies\Microsoft\WindowsPowerShell\3.0\System.Management.Automation.dllを追加します。

参照に アセンブリ > System.Management.Automationを加えます。

1.2 コード記述

usingに using System.Management.Automation;using System.Management; を加えて、以下のサンプルコードを書きます。hogehoge! と出力されるだけの意味のないものです。

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

namespace PowerShellClass1
{
    [Cmdlet(VerbsCommon.Get, "string")]
    public class GetString : Cmdlet
    {
        protected override void EndProcessing()
        {
            WriteObject("hogehoge!");
        }
    }
}
1.3 ビルド

ビルドします。

ここではPowerShell 4.0で動作させているので、対象フレームワークは .NET Framework 4.5としていますが、PowerShell3.0 なら .NET Framework 4.0となります。

1>------ すべてのリビルド開始: プロジェクト:PowerShellClass1, 構成:Debug Any CPU ------
1>  PowerShellClass1 -> D:\visual studio 2013\Projects\ClassLibrary1\ClassLibrary1\bin\Debug\PowerShellClass1.dll
========== すべてリビルド: 1 正常終了、0 失敗、0 スキップ ==========
1.4 PowerShellでバイナリからモジュールを読み込む

PowerShell から dllをパス指定してImport-Module します。

Import-Module PowerShellClass1.dll

結果モジュールが読み込まれます。

ModuleType Version    Name                                ExportedCommands
---------- -------    ----                                ----------------
Binary     1.0.0.0    PowerShellClass1                    Get-string
1.5 読み込んだコマンドを実行する
  • 実行してみると、記述通り hogehoge! と出力されます。
Get-string
hogehoge!

簡単な流れでしたが、C# で Cmdletを記述しビルドした.dll をPowerShellで読み込み、実行することができます。

VSと C# のサポートをフルに受けれるので、開発としては非常に楽なのは確かです。

ここまで詳解したスクリプトモジュール(.psm1) やバイナリモジュール(.dll) ですが、モジュール読み込み時に マニフェスト(.psd1) を利用することで「モジュールバージョン」や「作者情報」、「出力する Cmdletや変数」などの制御が可能です。

さっそくマニフェストモジュールによる、モジュール制御を見てみましょう。


III. Manifest Modules

マニフェストモジュールは、 先述したスクリプトモジュールやバイナリモジュールをマニフェスト(.psd1)を用いてきめ細かく制御します。

例えば、「モジュールのバージョン」や「作者情報」を考えても、スクリプトモジュール(.psm1)やバイナリモジュール(.dll)では、Import-Module でモジュールを読み込んだ時にバージョンなどを指定することができず、またモジュール内部で仕込む場所もありません。*2

他にもマニフェストファイル(.psd1) を利用することで、複数のスクリプトモジュールをまとめて一つのモジュールとして読み込ませたりも可能になります。(Nested Module)

PowerShell 4.0 からは、マニフェストに記述したバージョンを Get-Module や Import-Module で指定できるようになったため一層使いやすくなりました。

では具体的に、マニフェストファイルの生成から内容まで見ていきましょう。

マニフェストの生成

マニフェストの生成には、専用のCmdlet が用意されています。

New-ModuleManifest

まずはサンプルとして空のマニフェストを作ってみてみます。

New-ModuleManifest -path manufesttest.psd1
#
# モジュール 'manufesttest' のモジュール マニフェスト
#
# 生成者: guitarrapc
#
# 生成日: 2013/12/02
#

@{

# このマニフェストに関連付けられているスクリプト モジュール ファイルまたはバイナリ モジュール ファイル。
# RootModule = ''

# このモジュールのバージョン番号です。
ModuleVersion = '1.0'

# このモジュールを一意に識別するために使用される ID
GUID = '7a12ba32-5e12-4ee1-8685-948ecc9246df'

# このモジュールの作成者
Author = 'guitarrapc'

# このモジュールの会社またはベンダー
CompanyName = '不明'

# このモジュールの著作権情報
Copyright = '(c) 2013 guitarrapc. All rights reserved.'

# このモジュールの機能の説明
# Description = ''

# このモジュールに必要な Windows PowerShell エンジンの最小バージョン
# PowerShellVersion = ''

# このモジュールに必要な Windows PowerShell ホストの名前
# PowerShellHostName = ''

# このモジュールに必要な Windows PowerShell ホストの最小バージョン
# PowerShellHostVersion = ''

# このモジュールに必要な Microsoft .NET Framework の最小バージョン
# DotNetFrameworkVersion = ''

# このモジュールに必要な共通言語ランタイム (CLR) の最小バージョン
# CLRVersion = ''

# このモジュールに必要なプロセッサ アーキテクチャ (なし、X86、Amd64)
# ProcessorArchitecture = ''

# このモジュールをインポートする前にグローバル環境にインポートされている必要があるモジュール
# RequiredModules = @()

# このモジュールをインポートする前に読み込まれている必要があるアセンブリ
# RequiredAssemblies = @()

# このモジュールをインポートする前に呼び出し元の環境で実行されるスクリプト ファイル (.ps1)。
# ScriptsToProcess = @()

# このモジュールをインポートするときに読み込まれる型ファイル (.ps1xml)
# TypesToProcess = @()

# このモジュールをインポートするときに読み込まれる書式ファイル (.ps1xml)
# FormatsToProcess = @()

# RootModule/ModuleToProcess に指定されているモジュールの入れ子になったモジュールとしてインポートするモジュール
# NestedModules = @()

# このモジュールからエクスポートする関数
FunctionsToExport = '*'

# このモジュールからエクスポートするコマンドレット
CmdletsToExport = '*'

# このモジュールからエクスポートする変数
VariablesToExport = '*'

# このモジュールからエクスポートするエイリアス
AliasesToExport = '*'

# このモジュールに同梱されているすべてのモジュールのリスト
# ModuleList = @()

# このモジュールに同梱されているすべてのファイルのリスト
# FileList = @()

# RootModule/ModuleToProcess に指定されているモジュールに渡すプライベート データ
# PrivateData = ''

# このモジュールの HelpInfo URI
# HelpInfoURI = ''

# このモジュールからエクスポートされたコマンドの既定のプレフィックス。既定のプレフィックスをオーバーライドする場合は、Import-Module -Prefix を使用します。
# DefaultCommandPrefix = ''

}

マニフェストにRootModule(モジュールファイル .psm1や.dllを指定する)やModuleVersion(モジュールバージョンの指定)、FunctionsToExport(出力するファンクションを指定)を記述することで、Import-Moduleでモジュールが読み込まれるときに、読み込まれるモジュールを制御します。

マニフェストファイルを出力する

マニフェストファイル(.psd1)は、モジュールファイル(.psm1)同様、対象のモジュールフォルダ直下に配置します。(valentiaなら valentia\valentia.psm1 と valentia\valaneita.psd1 がモジュールフォルダ直下に存在する)

サンプルとして valentiaのマニフェストファイルを見てみましょう。

まず、マニフェストファイル(.psd1)を次のコマンドレットで出力しています。

$script:module = "valentia"
$script:moduleVersion = "0.3.3"
$script:description = "PowerShell Remote deployment library for Windows Servers";
$script:copyright = "28/June/2013 -"
$script:RequiredModules = @()
$script:clrVersion = "4.0.0.0" # .NET 4.0 with StandAlone Installer "4.0.30319.1008" or "4.0.30319.1" , "4.0.30319.17929" (Win8/2012)

$script:functionToExport = @(
        "ConvertTo-ValentiaTask",
        "Edit-ValentiaConfig",
        "Get-ValentiaCredential",
        "Get-ValentiaFileEncoding",
        "Get-ValentiaGroup", 
        "Get-ValentiaRebootRequiredStatus",
        "Get-ValentiaTask", 
        "Initialize-ValentiaEnvironment",
        "Invoke-Valentia",
        "Invoke-ValentiaAsync",
        "Invoke-ValentiaClean",
        "Invoke-ValentiaCommand",
        "Invoke-ValentiaCommandParallel", 
        "Invoke-ValentiaDeployGroupRemark",
        "Invoke-ValentiaDeployGroupUnremark",
        "Invoke-ValentiaDownload",
        "Invoke-ValentiaParallel", 
        "Invoke-ValentiaSync",
        "Invoke-ValentiaUpload", 
        "Invoke-ValentiaUploadList", 
        "New-ValentiaCredential", 
        "New-ValentiaGroup",
        "New-ValentiaFolder",
        "Set-ValentiaHostName",
        "Set-ValentiaLocation", 
        "Show-ValentiaConfig",
        "Show-ValentiaGroup",
        "Show-ValentiaPromptForChoice"
)

$script:variableToExport = "valentia"
$script:AliasesToExport = @("Task",
    "Valep","CommandP",
    "Vale","Command",
    "Valea",
    "Upload","UploadL",
    "Sync",
    "Download",
    "Go",
    "Clean","Reload",
    "Target",
    "ipremark","ipunremark",
    "Cred",
    "Rename",
    "Initial"
)

$script:moduleManufest = @{
    Path = "$module.psd1";
    Author = "guitarrapc";
    CompanyName = "guitarrapc"
    Copyright = ""; 
    ModuleVersion = $moduleVersion
    Description = $description
    PowerShellVersion = "3.0";
    DotNetFrameworkVersion = "4.0";
    ClrVersion = $clrVersion;
    RequiredModules = $RequiredModules;
    NestedModules = "$module.psm1";
    CmdletsToExport = "*";
    FunctionsToExport = $functionToExport
    VariablesToExport = $variableToExport;
    AliasesToExport = $AliasesToExport;
}

New-ModuleManifest @moduleManufest

出力された結果です。

#
# Module manifest for module 'valentia'
#
# Generated by: guitarrapc
#
# Generated on: 2013/12/02
#

@{

# Script module or binary module file associated with this manifest.
# RootModule = ''

# Version number of this module.
ModuleVersion = '0.3.3'

# ID used to uniquely identify this module
GUID = '87410785-c365-476c-9b56-ebe8ca259837'

# Author of this module
Author = 'guitarrapc'

# Company or vendor of this module
CompanyName = 'guitarrapc'

# Copyright statement for this module
Copyright = '(c) 2013 guitarrapc. All rights reserved.'

# Description of the functionality provided by this module
Description = 'PowerShell Remote deployment library for Windows Servers'

# Minimum version of the Windows PowerShell engine required by this module
PowerShellVersion = '3.0'

# Name of the Windows PowerShell host required by this module
# PowerShellHostName = ''

# Minimum version of the Windows PowerShell host required by this module
# PowerShellHostVersion = ''

# Minimum version of Microsoft .NET Framework required by this module
DotNetFrameworkVersion = '4.0'

# Minimum version of the common language runtime (CLR) required by this module
CLRVersion = '4.0.0.0'

# Processor architecture (None, X86, Amd64) required by this module
# ProcessorArchitecture = ''

# Modules that must be imported into the global environment prior to importing this module
# RequiredModules = @()

# Assemblies that must be loaded prior to importing this module
# RequiredAssemblies = @()

# Script files (.ps1) that are run in the caller's environment prior to importing this module.
# ScriptsToProcess = @()

# Type files (.ps1xml) to be loaded when importing this module
# TypesToProcess = @()

# Format files (.ps1xml) to be loaded when importing this module
# FormatsToProcess = @()

# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
NestedModules = @('valentia.psm1')

# Functions to export from this module
FunctionsToExport = 'ConvertTo-ValentiaTask', 'Edit-ValentiaConfig', 
               'Get-ValentiaCredential', 'Get-ValentiaFileEncoding', 
               'Get-ValentiaGroup', 'Get-ValentiaRebootRequiredStatus', 
               'Get-ValentiaTask', 'Initialize-ValentiaEnvironment', 
               'Invoke-Valentia', 'Invoke-ValentiaAsync', 'Invoke-ValentiaClean', 
               'Invoke-ValentiaCommand', 'Invoke-ValentiaCommandParallel', 
               'Invoke-ValentiaDeployGroupRemark', 
               'Invoke-ValentiaDeployGroupUnremark', 'Invoke-ValentiaDownload', 
               'Invoke-ValentiaParallel', 'Invoke-ValentiaSync', 
               'Invoke-ValentiaUpload', 'Invoke-ValentiaUploadList', 
               'New-ValentiaCredential', 'New-ValentiaGroup', 'New-ValentiaFolder', 
               'Set-ValentiaHostName', 'Set-ValentiaLocation', 'Show-ValentiaConfig', 
               'Show-ValentiaGroup', 'Show-ValentiaPromptForChoice'

# Cmdlets to export from this module
CmdletsToExport = '*'

# Variables to export from this module
VariablesToExport = 'valentia'

# Aliases to export from this module
AliasesToExport = 'Task', 'Valep', 'CommandP', 'Vale', 'Command', 'Valea', 'Upload', 'UploadL', 
               'Sync', 'Download', 'Go', 'Clean', 'Reload', 'Target', 'ipremark', 'ipunremark', 
               'Cred', 'Rename', 'Initial'

# List of all modules packaged with this module
# ModuleList = @()

# List of all files packaged with this module
# FileList = @()

# Private data to pass to the module specified in RootModule/ModuleToProcess
# PrivateData = ''

# HelpInfo URI of this module
# HelpInfoURI = ''

# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix.
# DefaultCommandPrefix = ''

}
マニフェストの注意点

各パラメータが ハッシュテーブル形式で記述されていることがわかると思います。 多くのパラメータは、読んでそのままですが注意を要する項目に触れておきましょう。

RootModule

このパラメータにバイナリモジュール(.dll)やモジュールファイル(.psm1)の名前(.psd1の同一パスに.ps1mが存在することが同一パスとなる)を指定することで、対象のモジュールをモジュールファイルの記述(あるいバイナリモジュールの記述)通りに読み込んでからマニフェスト(.psd1)の記述で制御します。

このパラメータで モジュールファイル(.psm1) を指定したとき、Import-Module で読み込まれるモジュールタイプは スクリプトモジュールとなります。

NestedModule

このパラメータは、モジュールファイル(.psm1)を複数指定することで、対象のモジュールをまとめてモジュールファイルの記述(あるいバイナリモジュールの記述)通りに読み込んでからマニフェスト(.psd1)の記述で制御します。

このパラメータで モジュールファイル(.psm1) を指定したとき、Import-Module で読み込まれるモジュールタイプは マニフェストモジュールとなります。

FunctionToExport

モジュールファイルなどで、Export-ModuleMemberで指定したファンクションの制御に利用します。

マニフェストファイル(.psd1)が存在するとき、モジュールファイル(.psm1)で指定され、かつマニフェストのこのパラメータで指定されたファンクションのみがモジュールファンクションとして出力されます。

ToExport は基本的に同様に働き、変数やAliasも制限が可能です。

他にも FormatsToProcessでの 書式設定ファイル(.formats.ps1xml) 指定もありますが、ここでは割愛します。

マニフェストでのモジュール読み込み

マニフェストであっても、モジュールの読み込み方法は同様です。

必ず、モジュールフォルダと同一の名称を付けて出力します。(valentiaの場合は、valentia.psd1)

この上で、モジュールファイル(.psm1)やバイナリモジュール(.dll)とともに配置されていることを確認してImport-Module することでモジュールがインポートされます。


IV. Dynamic Modules

最後は、Dynamic Module です。

ダイナミックモジュール は、これまであげたモジュールと一つ大きな違いがあります。

現在のPowerShell セッション内部/インメモリにモジュール生成する

PowerShellセッションが切れるとダイナミックモジュールも消えます。この恒久的ではなく一時的なモジュールという性質が他と大きく異なる点です。

まずは ダイナミックモジュールの生成を見てみましょう。

New-Moduleによるモジュール生成

ダイナミックモジュールを生成するには、New-Module コマンドレットを利用します。

functionとしてのダイナミックモジュール

例えば、以下のようなHelloと出すだけのダイナミックモジュールを実行するとWindowsで勝手にNameを付けてくれます。

New-module -Scriptblock {function Hello {"Hello!"}}
ModuleType Version    Name                                ExportedCommands
---------- -------    ----                                ----------------
Script     0.0        __DynamicModule_c3a44fd4-d78c-41... Hello
functionとしてのダイナミックモジュールに名称をつける

もしモジュールに名前を付けたい場合は、-Nameパラメータで指定します。

PS> New-module -Scriptblock {function test {"Hello!"}} -Name hoge

ModuleType Version    Name                                ExportedCommands
---------- -------    ----                                ----------------
Script     0.0        hoge                                test

CustomObjectとしてのダイナミックモジュール

あるいは、-AsCustomObject とすることでメソッドの追加も可能です。

$hoge = New-Module -Scriptblock {function hoge ($name) {"Hello, $name"}; function hogehoge ($name) {"Goodbye, $name"}} -AsCustomObject
$hoge | gm

このように、スクリプトメソッドを生成することができます。

   TypeName: System.Management.Automation.PSCustomObject

Name        MemberType   Definition
----        ----------   ----------
Equals      Method       bool Equals(System.Object obj)
GetHashCode Method       int GetHashCode()
GetType     Method       type GetType()
ToString    Method       string ToString()
hogehoge    ScriptMethod System.Object Goodbye();
hoge        ScriptMethod System.Object Hello();

実行してみましょう。

$hoge.hoge("guitarrapc")

メソッド実行できていますね。

Hello, guitarrapc
ダイナミックモジュール内部でfunctionを実行して結果だけを返す

最後に、-ReturnResultとすることで、スクリプトブロックの実行内容を返します。

New-Module -ScriptBlock {function test {"test!"}; test} -returnResult

結果、test ファンクションが実行された結果が変えります。

test!

ダイナミックモジュールは、その場限りのモジュールなため、私自身はそれほど使いませんが、繰り返し実行にはいいかも知れませんね。



モジュールへのコマンドレット配置手法

PowerShellスクリプト単独でモジュールを作る場合、モジュールのファンクションや動作をモジュールファイル(.psm1)で定め、マニフェスト(.psd1)で公開する情報を制御するマニフェストモジュールが最も利用されることでしょう。

モジュールファイルで複数のファンクションを読み込ませるにあたり、どのように配置するべきかを考えるとおよそ2つあるかと思います。

手法 概要 メリット デメリット
.psm1に直接書き込む .psm1内部でファンクションを直接記述 変数の受け渡しが容易 ファンクションが増加すると著しく可読性、保守性が落ちる
.ps1分散/.psm1にドットソース読込 .ps1に各ファンクションを記述し.psm1でドットソースで読み込む 機能別に分離しており可読性、保守性が良い 変数の受け渡しで工夫を要する

それぞれのメリットとデメリットを見てみましょう。

1. モジュールファイル(.psm1)に直接書き込む

モジュールファイル内部で、利用するファンクションを直接記述し、モジュールファイル(.psm1)の最後でExport-ModuleMemberにて制御する方法です。

この手法で開発されている有名なライブラリとしては、psakeがあります。

また、guitarrapc / PS-SumoLogicAPI もこの手法で記述しています。

メリット

非常に直観的で、変数などのインテリセンスも何も考えずとも.psm1さえ開いていれば効くため、書いている時は楽です。

デメリット

すべてのファンクションが一つのファイルに存在するのは、可読性を著しく下げます。 特にGitHubのようなバージョン管理ツールと相性が悪く、一部の機能でもファイル全体の更新という結果になります。

切り替えタイミング

3,4個のファンクションであったり、さくっと書いただけというかき捨てならばともかく、モジュールがある程度の規模になった場合は、「.ps1へ分散」を検討するべきです。

2. スクリプトファイル(.ps1)分散/モジュールファイル(.psm1)にドットソース読込

モジュールで利用するファンクションを、一つ一つスクリプトファイル(.ps1)に分散させ、モジュールファイル(.psm1)にはフォットソースで読み込む手法です。

この手法で開発されている有名なライブラリとしては、Pesterchocolateyがあります。

また、guitarrapc / valentia もこの手法で開発しています。

メリット

機能別に分離しており可読性、保守性が高くなります。 また、GitHubのようなバージョン管理ツールでも、変更があった箇所のみの更新となります。

デメリット

変数の受け渡しや、インテリセンスが .psm1に直接すべて記述するのに比べると難があります。

が、大した手間ではない上に、それにも勝る保守性の良さがあります。

スクリプトファイルの読み込み

さて、スクリプトファイル(.ps1) を分散させている場合、今度はその読み込みをどのように簡略化するか考える必要があります。

よく見かけるこれは、非推奨です。

. $psScriptRoot\hoge.ps1
. $psScriptRoot\fuga.ps1
. $psScriptRoot\piyo.ps1
. $psScriptRoot\foo.ps1
. $psScriptRoot\bar.ps1

なぜなら、スクリプトファイルが増えるごとに毎回メンテナンスが必要です。

functionを格納するフォルダを生成し、フィルタして自動的に読み込ませましょう。

Pesterの場合は、このようにしています。

Get-ChildItem $pester.fixtures_path -Include "*.ps1" -Recurse |
    ? { $_.Name -match "\.Tests\." } |
    % { & $_.PSPath }

valentiaの場合も、似たような形式です。

Resolve-Path (Join-Path $valentia.modulePath $valentia.helpersPath) | 
    where { -not ($_.ProviderPath.Contains(".Tests.")) } |
    % { . $_.ProviderPath }

まとめ

想定外(遅刻した!) の長さになってしまいましたが、おおよそモジュールに関する説明となりました。

プロジェクトの規模に応じて、スクリプトモジュール、マニフェストモジュールを選択し、メンテナンス性を考えてスクリプトファイル(.ps1)を配置、開発できるとよいかと思います。

それでは、明日(もう今日だ!) は、@rbtnnさんです。がんばってください!

*1:私はVisual Studio 2013で行っていますが、VS 2010/2012でも可能です。

*2:バイナリモジュールはdllのバージョンとして保持できますが、それは一端置いておきましょう