PowerShellにおけるモジュールの取扱い方

当記事ではPowerShellでモジュールを取り扱う方法を解説します。





## 1. モジュールとは

当記事で言うモジュールとは、自作した変数・関数・クラスを様々なスクリプトで再利用できるようにする仕組み、および当該の仕組みで参照されるファイル、とでも緩やかに定義しておきます。まぁ、何となく言わんとすることは分かってもらえるかと思います。

PowerShellでモジュールを利用する方法は、いくつかありますので順を追って紹介していきます。





## 2. スクリプトファイルで区切る

最も単純明快な方法と言えるのは、使いまわしたい処理をスクリプトファイルに格納し、当該スクリプトファイルを呼び出すという方法です。





### 2-1. 取扱い方

エントリポイントなるファイルをIndex.ps1とし、モジュールとして呼び出したいファイルをModule.ps1とします。それらを以下のように配置します。一応言っておきますが、これはあくまで一例であり、必ずしもこのような配置にしなければならないということではありません。

```
■ .\
┣■ Index.ps1
┗■ lib
 ┗■ Module.ps1
```

使いまわしたい処理をモジュールとなるファイル（ここではModule.ps1）に記述します。今回は、受け取った数値を2倍にして返す、という処理を実装してみました。

```powershell:Module.ps1
# $Args はコマンドライン引数を格納する特殊な配列です
# $Args[0]には第1コマンドライン引数が格納されています
Return $Args[0] * 2
```

このモジュールは、呼び出したいファイルから絶対パス・相対パスで指定することで利用することができます。

```powershell:Index.ps1
# & は呼び出し演算子、あるいは起動演算子と呼ばれるものです
# $PSScriptRoot は自身の配置されているディレクトリまでの絶対パスが格納されている特殊な変数です
$ReturnValue = & "$($PSScriptRoot)\lib\Module.ps1" 2

# ここでは「4」と標準出力されます
Write-Output $ReturnValue
```





### 2-2. 所見

この方法の面白いところは、モジュールとなるファイルそのものを1つの関数のように、あるいは関数ブロックのように利用していることです。つまり、以下のように書いているのと、あまり変わらないということですね。

```powershell
Function Module($Argument) {
    Return $Argument * 2
}

$ReturnValue = Module 2
Write-Output $ReturnValue
```

1つ違うのは、モジュールとなるファイルで関数が定義できるため、結果的に、関数内関数のようなものを利用できることでしょう。例えば、モジュール内で関数を定義する場合、以下1つ目のようにソースコードになるわけですが、それは以下2つ目のようなソースコードと同義なのです。

JavaScriptなどのプログラミング言語とは異なり、PowerShellでは関数内で関数を定義することができませんので、便利と言えば便利かもしれません。

```powershell:1つ目
Function Sample() {
    ...
}

Return Sample
```

```powershell:2つ目
Function Module() {
    Function Sample() {
        ...
    }

    Return Sample
}
```

ただ、正直な話、この方法によるモジュール化には旨味がありません。一般的に、モジュールというものは内部に自作した関数やクラスを配置して、それを複数のファイルで使いまわすために存在していますが、この方法ではそういった用途には使えないのです。何せ、呼び出した瞬間に処理が全て走ってしまいますので、モジュールとなるファイル内で定義された関数やクラスは呼び出し元に持ってこれないからです。

クラスのインスタンスであれば返すことができますが、そのことに、どれだけの価値があるでしょうか。

```powershell:Module.ps1
Class Sample {
    $Value
    Sample($Argument) {
        $This.Value = $Argument
    }
}

$Instance = [Sample]::New(123)
Return $Instance
```

```powershell:Index.ps1
$ReturnObject = & "$($PSScriptRoot)\lib\Module.ps1"

# 123 と標準出力されます
Write-Output $ReturnObject.Value
```





## 3. ドットソース演算子を利用する

使いまわしたい処理をスクリプトファイルに記述し、それを呼び出すのは旨味がない方法です。それよりも、ドットソース演算子を利用して、モジュールを読み込むようにしましょう。

この方法では呼び出した瞬間に処理を走らせることはできませんが、モジュール内で定義した変数・関数・クラスを呼び出し元で使うことができるようになります。





### 3-1. 取扱い方

まずはエントリポイントとなるファイルであるIndex.ps1と、モジュールとなるファイルであるModule.ps1を以下のように配置します。

```
■ .\
┣■ Index.ps1
┗■ lib
 ┗■ Module.ps1
```

Module.ps1に使いまわしたい変数・関数・クラスを記述します。間違っても何らかの処理を書いてはいけません。あくまで変数・関数・クラスの定義だけです。今回は変数・関数・クラスを1つずつ格納してみます。

```powershell:Module.ps1
# 変数
$ModuleVariable = "モジュール変数"

# 関数
Function ModuleFunction() {
    Return "モジュール関数"
}

# クラス
Class ModuleClass {
    $Value
    ModuleClass() {
        $This.Value = "モジュールクラス"
    }
}
```

モジュールに格納された変数・関数・クラスを参照したいときは、冒頭で書きましたドットソース演算子を使用します。`.`の後ろに読み込みたいモジュールのパスを指定するのです。すると、読み込み元（今回はIndex.ps1）で、モジュール内の変数・関数・クラスを使用することができます。

なお、読み込んだモジュール間で名前が衝突した場合は、後から読み込んだモジュール内で定義された値で上書きされます。

```powershell:Index.ps1
. "$($PSScriptRoot)\lib\Module.ps1"

# モジュール変数 と出力されます
Write-Output $ModuleVariable

# モジュール関数 と出力されます
Write-Output (ModuleFunction)

# モジュールクラス と出力されます
$Instance = [ModuleClass]::New()
Write-Output $Instance.Value
```





### 3-2. 所見

呼び出し演算子でスクリプトファイルを呼び出すよりも、こちらのほうが一般的なモジュールに近い使い勝手であるはずです。

ただし、このドットソース演算子を利用した方法では、読み込み対象となるファイルで定義された全ての変数・関数・クラスが読み込まれてしまいます。読み込む対象を読み込み側が選んだり、外部に出す対象を指定するようなことはできません。





## 4. スクリプトモジュールについて

次の方法を説明する前に、スクリプトモジュールというモジュールの定義方法について解説します。

スクリプトモジュールとはモジュールの作り方の一種で以下のような特徴があります。

- モジュールとなるファイルには.psm1拡張子を付ける
- モジュールとなるファイル名と同じ名前のディレクトリの直下に配置する
- 実行可能なファイルとして認識されず、モジュール専用のファイルとなる

例えば、Module.psm1というスクリプトモジュールは、Moduleというディレクトリに直下に配置する必要があります。





## 5. Import-Module

ドットソース演算子によるモジュールの読み込みと似ていますが、より洗練されているのは`Import-Module`コマンドレットを用いた方法です。





### 5-1. 取扱い方

エントリポイントとなるIndex.ps1と、スクリプトモジュールであるModuleを以下のように配置します。

```
■ .\
■ Index.ps1
┗■ Module
 ┗■ Module.psm1
```

Module.psm1には以下のように記述します。

```powershell
Function Get-Integer() {
    Return 123
}

Function Get-String() {
    Return "Hello world."
}
```

Index.ps1内で`Import-Module`コマンドレットを使って、Module.psm1を読み込みます。このとき注意するべきは`Import-Module`コマンドレットの引数にモジュールのパスではなく、モジュールを格納したディレクトリのパスを指定することです。読み込んだ後は、モジュール内で定義された関数を自由に参照することができます。

```powershell
Import-Module "$($PSScriptRoot)\Module"

# 123 と表示されます
Write-Output (Get-Integer)

# Hello world. と表示されます
Write-Output (Get-String)
```

ある特定の関数だけを読み込みたいという場合は`Import-Module`コマンドレットの`-Function`オプションを使用してください。

```powershell
Import-Module "$($PSScriptRoot)\Module" -Function "Get-Integer"

# 123 と表示されます
Write-Output (Get-Integer)

# 読み込まれていないのでエラーになります
Write-Output (Get-String)
```

あるいはスクリプトモジュール側で外部に出す関数を指定することもできます。そのときは`Export-ModuleMember`コマンドレットを使用します。スクリプトモジュールの末尾で、どの関数を外に出すかを指定します。

```powershell
Function Get-Integer() {
    Return 123
}

Function Get-String() {
    Return "Hello world."
}

# Get-String だけを外部に出します
Export-ModuleMember "Get-String"
```

さて、ここまでの説明で気付いた人もいらっしゃるでしょうが、原則として、`Import-Module`コマンドレットと`Export-ModuleMember`コマンドレットが取り扱うのは関数です。変数を読み込む、あるいは外部に出す場合は以下のような記述が別途必要です。どちらか片方だけでは駄目で、スクリプト側・モジュール側双方で許可を出しておく必要があります。

```powershell:スクリプト側
Import-Module モジュールのパス -Variable 読み込む変数
```

```powershell:モジュール側
Export-ModuleMember -Variable 外部に出す変数
```

なお、この方法では、クラスは取扱い対象外ですので注意してください。





### 5-2. 所見

クラスが取り扱えない点を除けば、一般的なモジュール操作と言えるのではないでしょうか。

読み込む対象を絞りたい、あるいは外部に出す対象を限定したい、という欲求がないのであれば、ドットソース演算子を利用するほうが楽でしょう。





## 6. 暗黙的Import-Module

実は、PowerShellには暗黙的にスクリプトモジュールを読み込む仕様があります。`Import-Module`コマンドレットを使わずとも、ソースコード中にスクリプトモジュール内で定義された関数が呼び出されたときは、自動的に読み込んでくれるのです。





### 6-1. 取扱い方法

暗黙的読み込んで欲しいスクリプトモジュールは特定のディレクトリ内に配置する必要があります。具体的には`$env:PSModulePath`という特殊な変数に格納された場所に配置することで暗黙的な読み込みが発生します。

以下は私の環境における配置場所です。

```
PS C:\Users\AGadget> $env:PSModulePath
C:\Users\AGadget\Documents\WindowsPowerShell\Modules;C:\Program Files\WindowsPowerShell\Modules;C:\Windows\system32\WindowsPowerShell\v1.0\Modules
```

随分と長いメッセージですが、よく見るとセミコロンで区切られた3つの絶対パスであると分かります。分解すると以下の通りになります。

- C:\Users\AGadget\Documents\WindowsPowerShell\Modules
- C:\Program Files\WindowsPowerShell\Modules
- C:\Windows\system32\WindowsPowerShell\v1.0\Modules

この3ヵ所の、いずれか1つに配置すればOKです。この変数にパスを追加することもできます。

ちなみに、それぞれに同名のスクリプトモジュールを配置した場合、上位（メッセージの先頭）に位置するパスから優先して読み込まれます。





### 6-2. 所見

`Import-Module`コマンドレットを書かなくて済むので便利です。