作業効率化のために、Visual Studio Code で PowerShell Coreのモジュールを作ってみる

はじめに

初めまして。今年4月から次世代デジタル基盤開発事業部に籍を置くことになった I と申します。 こちらでは、Webプログラミングを中心に携わっております。

業務としての開発のみならず、作業や業務の効率化を開発者の視点で推進できるように試行錯誤しております。

PowerShellで便利なコマンドを簡単に作ってみるための手順などを書いていこうと思います。

目次

Windowsの強力な味方、PowerShell

さて、Windowsで作業されている皆さんにとって、PowerShellというCLI(とシェルスクリプト実行環境)を目にする機会は多いと思われます。

learn.microsoft.com

もともと、Windowsには コマンドプロンプト(cmd) というMS-DOS譲りのCLIが存在していましたが、Windows 7の頃にMicrosoftが用意したものが起源です。

PowerShellには、コマンドプロンプトには無い、魅力的な機能が備わっていました。

1. コマンドレットの存在

PowerShellでは、コマンドのことを コマンドレット と呼んでいます。 *1 コマンドレットは基本的に以下のもので構成されています。

  • 実行ファイル
  • バッチファイル
  • PowerShell関数(スクリプトで記述)
  • .NETアセンブリ

また、出力される結果も、従来のシェルでは文字列(かその集合)でしたが、PowerShellではオブジェクトとして出力されます。

2. パイプライン処理

PowerShellでは、関数とコマンドの区別がありません。コマンドライン単体で呼びだせますし、関数内でも呼び出せます。 コマンドレットが処理した結果を、パイプ( | )を使用して別のコマンドレットに引き継げます。 コマンドプロンプトや bash などでもできますが、PowerShellでは文字列ではなくオブジェクトを渡しています。

例えば、コマンドプロンプトでいう dir に相当する Get-childItem コマンドを使用して、その結果を見てみます。

PS C:\Users\sample\Documents> Get-childItem .

    Directory: C:\Users\sample\Documents

Mode  LastWriteTime         Length Name
----  -------------              ------   ----
d---- 2023/04/04 17:34               dir1
d---- 2023/04/14 13:08               dir2
d---- 2023/04/20   9:38               dir3
-a--- 2023/04/04 18:16 348699 file1.txt
-a--- 2023/04/04 18:16   18617 file2.txt

このような出力だとピンと来ないと思われます。でも実は、これらはJavascriptでいう Object のような構成です。 仮に、JSON出力で出力してみた場合を想定すると、以下のようなイメージになります。

[
  { Mode: 'd----', LastWriteTime: '2023/04/04 17:34', Name: 'dir1' },
  { Mode: 'd----', LastWriteTime: '2023/04/14 13:08', Name: 'dir2' },
  { Mode: 'd----', LastWriteTime: '2023/04/20 9:38', Name: 'dir3' },
  { Mode: '-a---', LastWriteTime: '2023/04/04 18:16', Length: 348699, Name: 'file1.txt' },
  { Mode: '-a---', LastWriteTime: '2023/04/04 18:16', Length: 18617, Name: 'file2.txt' },
]

これを、パイプを使ってオブジェクトをフィルタリングしてみます。

Get-childItem . | Where-Object {$_.Mode -eq "d----" }

すると、結果は Mode: 'd----'のもの(つまりディレクトリ)だけがフィルタリングされます。

Mode  LastWriteTime         Length Name
----  -------------              ------   ----
d---- 2023/04/04 17:34               dir1
d---- 2023/04/14 13:08               dir2
d---- 2023/04/20   9:38               dir3

JSON化イメージではこうなります。

[
  { Mode: 'd----', LastWriteTime: '2023/04/04 17:34', Name: 'dir1' },
  { Mode: 'd----', LastWriteTime: '2023/04/14 13:08', Name: 'dir2' },
  { Mode: 'd----', LastWriteTime: '2023/04/20 9:38', Name: 'dir3' },
]

もちろん、さらにパイプを繋げてさらにフィルタリングできますし、変数にこのリストを紐づけできます。

$directories = Get-childItem . | Where-Object {$_.Mode -eq "d----" }

3. 関数が書きやすい

関数の書き方がCライクになり、構造がわかりやすくなっています。

例として、Linuxでの rm -irf をPowerShellで書いてみます。

function Rmirf {
  Remove-Item -Recurse -Force $args
};

このスクリプトをPowerShellにわかりやすい場所に保存しておけば、以下のコマンドで rm -irfを実行できます。

Rmirf ./dir1

4. オープンソース版がある

現在、PowerShellの最新バージョンは7です。とはいえ、Windows標準のPowerShellは5なのです。これは一体どういうことなのでしょうか。

実は、Powershellは現在2系統に分かれており、Windows標準のものとは別に、PowerShell Core というものが存在しています。 こちらは、Githubで公開されているオープンソースソフトウェアで、マルチプラットフォームでの利用が可能です*2

github.com

Windows標準のほうは最新版が2017年ですので、筆者は更新が活発なPowerShell Core 7(Windows(x64)用)を愛用しています。本記事でも、こちらのバージョンを前提に記載いたします。

5. モジュールをまとめたサイトがある

PowerShellのモジュールは便利で、外部の人が作ったモジュールを取り込めます。 しかし、配布している場所がバラバラだと利用者の管理が不便になります。 そこでお勧めのサイトは、マイクロソフトが運営している PowerShell Gallery です。

www.powershellgallery.com

ここから使えそうなモジュールを調べて、Install-Module コマンドで取り込めます。

PowerShell コマンドの開発を Visual Studio Code で実践する

ここからは、実際にPowerShellのコマンドレットを開発していきます。 直接関数を書いてもよいのですが、取り回しがきく モジュール 形式で開発していきます。

モジュールに関しては、Microsoftによる解説を読んでいただければ幸いですが、本記事では、そこまで難しく考える必要はございません。 使う関数をまとめたもの という認識で問題ないと思います。

learn.microsoft.com

手順を読むにあたって

今回はモジュールを作る例として、何らかのインストールを簡単にする EasyInstall というモジュールを作ろうとしていると想定しています。

Visual Studio Codeの拡張を導入する

Visual Studio Codeには、PowerShellモジュール開発に便利な拡張機能がMicrosoftによって導入されています。 とにかく、この拡張を 必ず インストールすることをお願いいたします。

marketplace.visualstudio.com

モジュールを配置するディレクトリの準備

PowerShellのスクリプトなどを格納するディレクトリは大体決まっております。 対象のディレクトリは複数あり、PowerShell本体は、それらのディレクトリを検索してモジュールなどを取り込んでおります。 なので、開発しているスクリプトを保存しておくときは、そこに置いておいたほうが無難です。

開発にお勧めのディレクトリは以下の場所です。

c:\Users\(ユーザー名)\Documents\PowerShell\Modules

ディレクトリが存在していない場合はあらかじめ作成をお願いいたします。

モジュールのディレクトリを作成

上記ディレクトリを作成しましたら、その直下にモジュール名を記したディレクトリを作成します。 ここで注意すべきなのは、作成するスクリプトファイル名と同じディレクトリ名にしておくことです。

EasyInstall で例えると、ディレクトリ名は EasyInstall となり、モジュールファイル名は EasyInstall.psm1 となります。 最終的なモジュールファイルのパスは、

c:\Users\(ユーザー名)\Documents\PowerShell\Modules\EasyInstall\EasyInstall.psm1

となります。

こうしておかないと、実際にそのモジュールを取り込もうとする時にエラーが発生します。

ディレクトリを作成したら、そのディレクトリのフルパスをメモしておいてください(クリップボードでもOK)。 このパスは後々使うことになります。

PowerShell拡張を起動

ここまでできたら、再びVisual Studio Codeの出番です。PowerShell拡張を起動します。 モジュール作成には Plaster というツールを使用します。

まずは、 Ctrl+Shift+P キーを押して、拡張メニューを開きます。

リストに PowerShell: Create New Project from Plaster Template というものがありますので、それを選択します。

モジュール環境の構築

選択すると、モジュールを構築する際に、どのテンプレートを使うのかを指示されます。

ここでは、New PowerShell Manifest Module を選択します。

次に、モジュールのパス名を促されるので、先にメモしておいたパスを記入します。 今回の例では、次のパスを入力します。

c:\Users\(ユーザー名)\Documents\PowerShell\Modules\EasyInstall

記入して Enter すると、ターミナルが開いてPlasterが起動し、モジュール環境を自動的に構築してくれます。

ここからはターミナルでの作業となります。

モジュールの設定を記述

ここからはモジュールの設定となりますが、重要な設定は以下の二つのみです。

(1)モジュール名

先ほど決めたモジュール名です。 今回の例では EasyInstall ですね。

(2)開発環境

[C]Visual Studio Code を選びます。

あとはエンターキーで省略してください(まず使いません)。

設定が完了したら、VSCodeの新しいウインドウが開きます。

関数の実装

続いて、コマンドレットとして実行させたい関数を実装していきます。 非常に単純な例として、標準出力に Hoge と表示させるだけの PrintHoge というコマンドを作ります。

EasyInstall.psm1を開き、# Implement your module commands in this script. の下で以下のコードを記述します。

function PrintHoge {
    Write-Output "Hoge";
}

また、最後の行を編集して、明示的に PrintHoge をエクスポートするようにしておきます。

Export-ModuleMember -Function PrintHoge

書き終えたら保存してコンソールに移ります。

モジュールのインストール

ここまで来たらあとは関数をコマンドとして呼び出すだけです。

Import-Module EasyInstall

なにも出力されなければインポートは成功です。

モジュールのインポート確認

このままコマンドを実行してもよいのですが、やはり「本当に入ってるの?」と疑問を呈されるのはごもっともです。 Get-Module コマンドでインポートされているかどうかが確認できます。

Get-Module -ListAvailable -Name EasyInstall

コマンドの実行

最後に、PrintHoge と打ち込めばコンソールに Hoge と表示されます。

お疲れさまでした。

関数の簡単な解説

…と、このまま終わってしまっては先へ進めなくなるので、簡単ですが関数の内容をざっくりと解説します。

関数の引数

関数の引数を指定するときは Param 命令を使います。 引数に値を渡すと省略時にその値を使用します。 ここらへんはJavascriptなどと同じですね。

function Grep {
    Param ($regex, $dir = ".")
    Get-ChildItem $dir | Select-String -Pattern $regex
}

ただし、可変長引数を扱う際は $args グローバル変数と配列展開を使用します。

function RmIrf {
    Remove-Item -Recurse -Force $args
};

Paramsと組み合わせて、複雑な引数構成を実装できます。

変数について

PowerShellでの変数は接頭辞に $ を付けておきます。 基本的に変数はオブジェクトだと認識していただけたらOKです。 例えば、以下のコードのようにすれば、モジュール一覧を変数として保持できます。 Javascript的に説明すると Objectの配列 です。

    $modules = Get-Module -ListAvailable

例として、Windows標準PowerShellに入っているモジュールをPowerShell Coreで検索した結果を示してみます。

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

ModuleType Version    PreRelease Name                                PSEdition ExportedCommands
---------- -------    ---------- ----                                --------- ----------------
Manifest   1.0.0.0               AppBackgroundTask                   Core,Desk {Disable-AppBackgroundTaskDiagnosticLog, Enable-AppBackgroundTaskDiagnosticLog, Set-AppBackgroundTaskResourcePo… 
Script     1.0.0.0               AssignedAccess                      Core,Desk {Clear-AssignedAccess, Get-AssignedAccess, Set-AssignedAccess}
Manifest   1.0.0.0               BitLocker                           Core,Desk {Unlock-BitLocker, Suspend-BitLocker, Resume-BitLocker, Remove-BitLockerKeyProtector…}
Script     2.0.0.0               BitsTransfer                        Core,Desk {Add-BitsFile, Complete-BitsTransfer, Get-BitsTransfer, Remove-BitsTransfer…}
Manifest   1.0.0.0               BranchCache                         Core,Desk {Add-BCDataCacheExtension, Clear-BCCache, Disable-BC, Disable-BCDowngrading…}
(略)

そして、改めてフィルタリングもできます。 先にも書きましたが、モジュール一覧を指定の名前でフィルタリングする場合は以下のように記述します。

    $moduleNames = $modules | Where-Object {$_.Name -eq $moduleName}

一覧のName属性が、指定の名前と同じものだけを抽出して、新しい配列を作っているイメージです。 比較演算子は bash などを嗜んでいらっしゃればすぐに理解できると思います。

ちょっと例を示してみます*3$modules に紐づけた内容から、バージョンが 1.0.0.0 のものだけを抽出してみます。

$modules = Get-Module -ListAvailable
$modules | Where-Object {$_.Version -eq "1.0.0.0"}

すると、結果はこのようになります。実際に Version のカラムが 1.0.0.0 のものばかりになっているのがお分かりになると思います。

PS C:\Users\***\source> $modules | Where-Object {$_.Version -eq "1.0.0.0"}

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

ModuleType Version    PreRelease Name                                PSEdition ExportedCommands
---------- -------    ---------- ----                                --------- ----------------
Manifest   1.0.0.0               AppBackgroundTask                   Core,Desk {Disable-AppBackgroundTaskDiagnosticLog, Enable-AppBackgroundTaskDiagnosticLog, Set-AppBackgroundTaskResourcePo… 
Script     1.0.0.0               AssignedAccess                      Core,Desk {Clear-AssignedAccess, Get-AssignedAccess, Set-AssignedAccess}
Manifest   1.0.0.0               BitLocker                           Core,Desk {Unlock-BitLocker, Suspend-BitLocker, Resume-BitLocker, Remove-BitLockerKeyProtector…}
(略)

まとめ

今回は、PowerShell Coreを使用して便利なコマンドを作ってみました。 標準で用意されているコマンドレットを組み合わせたりすることでもっと作業が捗ることを願います。

おまけ1: モジュールの再インポートを便利にするコマンドレット

さて、ここまでモジュールのインポートまで実践してみましたが、一つ面倒くさいことがあります。それは、モジュールを再インポートする場合、いったんそのモジュールを明示的に解除する必要があることです。

というのも、一旦実装してみてエラーがあった時や、思ったような結果にならずに修正したい時、未知の要素を盛り込みたくて試行錯誤したい時に地味に操作が面倒くさくなります*4

コマンドを一発打つだけで一括で処理できればうれしいので、それをやってくれるコマンドレットを作ってみました。

# 指定のモジュールを(再)インポート
# すでにインポートされていれば、いったん解除してインポート
function ReImportModule {
    Param ($moduleName)

    $modules = Get-Module -ListAvailable
    $moduleNames = $modules | Where-Object {$_.Name -eq $moduleName}

    # モジュールが存在していればまず削除
    If($modulenames.Count -eq 0) {
        Write-Output "This module is not imported. Import only : ${moduleName}"
    }
    Else {
        Remove-Module $moduleName
        Write-Output "Removed Module : ${moduleName}"
    }
    Import-Module $moduleName
    Write-Output "Imported Module : ${moduleName}"
}

これを一旦モジュール化してインポートしておけば、あとはコマンドを1回打つだけで再インストールができるようになります。

ReImportModule EasyShell

おまけ2: Linux風のコマンドで入力できる関数一覧

ls -la

function LsLa {
    Get-ChildItem -Force
}

rm -rf

function RmRf {
    Remove-Item -Recurse -Confirm $args
};

rm -irf

function RmIrf {
    Remove-Item -Recurse -Force $args
};

grep

function Grep {
    Param ($regex, $dir = ".")
    Get-ChildItem $dir | Select-String -Pattern $regex
}

grep -v

function GrepV {
    Param ($regex, $dir = ".")
    Get-ChildItem $dir | Select-String -NotMatch $regex
}

grep -r

function GrepR {
    Param ($regex, $dir = ".")
    Get-ChildItem -Recurse $dir | Select-String -Pattern $regex
}

grep -rv

function GrepRV {
    Param ($regex, $dir = ".")
    Get-ChildItem -Recurse $dir | Select-String -NotMatch $regex
}

tecotec.co.jp

*1:従来のコマンドも呼び出せますが、これらはコマンドレットのエイリアスです

*2:2023年5月現在では、Windows、MacOS、Ubuntu、Debian、CentOS、Red Hat Enterprise Linux、openSUSE、Fedora。ほかにはコミュニティベースのパッケージも存在している

*3:コマンドラインでも変数が使えるので、今回はコマンドラインに直で記述します

*4:もちろん、PowerShellにもヒストリー機能がございますので誤解のなきよう…何ならサジェスト機能もございます