【PowerShell / .NET】Windows PowerShell を自分好みにカスタマイズする

※ この記事は 2022年5月 に作成したものを一部改稿したものです。

**PowerShell** は主に Windows のシステム管理や自動化のために開発されたコマンドラインインターフェースおよびスクリプト言語で、Windows 7 から標準で提供されています。  

Windows には PowerShell の登場以前から コマンドプロンプト (cmd.exe) が搭載されていますが、GUIで提供されるような管理機能の自動化や再利用可能なスクリプトの作成には不向きで、PowerShell はこうした課題を解決するものとして開発されました。

2016年にはオープンソース&クロスプラットフォーム化した PowerShell Core が発表され、Linux や Mac でも利用できるようになりましたが、今回は Windows で提供されている Windows PowerShell をターゲットとし、快適に利用するためのカスタマイズ方法について考察します。

筆者の環境の Windows PowerShell のバージョンは以下の通りです。

```
PS C:\Users\xxx > $PSVersionTable

Name                           Value
----                           -----
PSVersion                      5.1.19041.1682
PSEdition                      Desktop
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}
BuildVersion                   10.0.19041.1682
CLRVersion                     4.0.30319.42000
WSManStackVersion              3.0
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
```

## PowerShell スクリプトの実行

本題に入る前に、まずは PowerShell スクリプトの実行方法について確認しておきます。

以下のコードをコピーし、`hello.ps1` などの名前で保存します。(`.ps1` は PowerShell スクリプトに用いられる拡張子です。)

```powershell
Write-Host 'Hello, world!'
```

Windows PowerShell を起動し、`cd` コマンドで `hello.ps1` があるディレクトリに移動します。

```
PS C:\Users\xxx> cd .\sandbox\
PS C:\Users\xxx\sandbox> Get-ChildItem


    ディレクトリ: C:\Users\xxx\sandbox


Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----        2022/05/27     18:31             27 hello.ps1
```

ディレクトリを移動したら、以下のようにして `hello.ps1` を実行します。

```
PS C:\Users\xxx\sandbox> .\hello.ps1
.\hello.ps1 : このシステムではスクリプトの実行が無効になっているため、ファイル C:\Users\xxx\sandbox\hello.ps1 を読み込むことができません。詳細については、「about_Execution_Policies」(https://go.microsoft.com/fwlink/?LinkID=135170) を参照してください。
発生場所 行:1 文字:1
+ .\hello.ps1
+ ~~~~~~~~~~~
    + CategoryInfo          : セキュリティ エラー: (: ) []、PSSecurityException
    + FullyQualifiedErrorId : UnauthorizedAccess
```

赤文字で上記のようなエラーメッセージが表示され、実行に失敗した方が多いかもしれません。

PowerShell は強力なツールであり、悪意のあるスクリプトの実行を防ぐため、Windows のクライアントPCではデフォルトではスクリプトの実行を許可しない設定になっています。  
スクリプトの実行を許可するには、Windows PowerShell を管理者権限で立ち上げて以下のコマンドを実行し、実行ポリシーを変更します。
(この変更は元に戻すことが可能です。)

```
PS C:\Windows\system32> Set-ExecutionPolicy RemoteSigned

実行ポリシーの変更
実行ポリシーは、信頼されていないスクリプトからの保護に役立ちます。実行ポリシーを変更すると、about_Execution_Policies のヘルプトピック (https://go.microsoft.com/fwlink/?LinkID=135170) で説明されているセキュリティ上の危険にさらされる可能性があります。実行ポリシーを変更しますか?
[Y] はい(Y)  [A] すべて続行(A)  [N] いいえ(N)  [L] すべて無視(L)  [S] 中断(S)  [?] ヘルプ (既定値は "N"): Y
```

`RemoteSigned` は、「インターネットからダウンロードしたスクリプトを実行するには信頼できる発行元からのデジタル署名が必要だが、ローカルPC上で作成したスクリプトは署名を必要としない」という実行ポリシーです。
確認のプロンプトが表示されたら、「Y」を入力して [Enter] を押します。

`Get-ExecutionPolicy` を実行し、`RemoteSigned` と表示されれば正常に実行ポリシーを変更できています。

```
PS C:\Windows\system32> Get-ExecutionPolicy
RemoteSigned
```

元のシェルに戻り、再度 `hello.ps1` を実行します。

```
PS C:\Users\xxx\sandbox> .\hello.ps1
Hello, world!
```

「Hello, world!」と表示されればOKです。

## PowerShell プロファイルの作成

ここからが本題です。

Linux などで Bash を使用している方の中には、ホームディレクトリに `.bash_profile` や `.bashrc` などのファイルを作成して環境変数やエイリアス (別名) の設定をしたことがある方が多いのではないでしょうか。(Zsh だと `.zprofile` と `.zshrc`)

PowerShell でも、同様に開始時に読み込まれる PowerShell プロファイルを作成して、エイリアスの設定や関数の定義などをすることが可能です。  
現在のユーザでログオン時に読み込まれるプロファイルのパスは、変数 `$PROFILE` で確認することができます。

```
PS C:\Users\xxx\sandbox> $PROFILE
C:\Users\xxx\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1
```

以下のコードをコピー&ペーストして実行すると、プロファイルが作成されます。

```powershell
if (!(Test-Path -Path $PROFILE)) {
    New-Item -ItemType File -Path $PROFILE -Force
}
```

```
PS C:\Users\xxx\sandbox> if (!(Test-Path -Path $PROFILE)) {
>>     New-Item -ItemType File -Path $PROFILE -Force
>> }


    ディレクトリ: C:\Users\xxx\Documents\WindowsPowerShell


Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----        2022/05/27     19:58              0 Microsoft.PowerShell_profile.ps1
```

このファイルを編集することで、PowerShell を自分好みにカスタマイズすることができます。

## PowerShell プロファイルの設定例

2022/05/27 現在の私の PowerShell プロファイルは、以下の内容となっています。

```powershell:Microsoft.PowerShell_profile.ps1
function Edit-Hosts {
    Start-Process `
        -FilePath wsl.exe `
        -ArgumentList 'vim /mnt/c/Windows/System32/drivers/etc/hosts' `
        -Verb RunAs
}

function Get-EnvironmentVariables {
    [Environment]::GetEnvironmentVariables().GetEnumerator() | Sort-Object -Property Key
}

function Edit-EnvironmentVariables {
    Start-Process `
        -FilePath rundll32.exe `
        -ArgumentList 'sysdm.cpl,EditEnvironmentVariables' `
        -Verb RunAs
}

function Export-JavaHome ([int] $version) {
    switch ($version) {
        8 {
            $dir = 'C:\langs\java\jdk8u312-b07'
        }
        11 {
            $dir = 'C:\langs\java\jdk-11.0.14.101-hotspot'
        }
        17 {
            $dir = 'C:\langs\java\jdk-17.0.2.8-hotspot'
        }
        default {
            Write-Host 'Error: Invalid Java version'
            return
        }
    }
    Start-Process `
        -FilePath powershell.exe `
        -ArgumentList "-Command [Environment]::SetEnvironmentVariable('JAVA_HOME', '${dir}', [EnvironmentVariableTarget]::Machine)" `
        -Verb RunAs
}

function prompt {
    $principal = [Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()
    if ($principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
        $role = 'Admin'
    } else {
        $role = 'User'
    }
    '[{0}] {1} > ' -f $role, ((Get-Location).Path -split '::')[-1]
}

Set-Alias hosts Edit-Hosts
Set-Alias env Edit-EnvironmentVariables

Set-PSReadlineKeyHandler -Chord Alt+b -Function BackwardWord
Set-PSReadlineKeyHandler -Chord Alt+d -Function KillWord
Set-PSReadlineKeyHandler -Chord Alt+f -Function ForwardWord
Set-PSReadlineKeyHandler -Chord Ctrl+a -Function BeginningOfLine
Set-PSReadlineKeyHandler -Chord Ctrl+d -Function DeleteCharOrExit
Set-PSReadlineKeyHandler -Chord Ctrl+e -Function EndOfLine
Set-PSReadlineKeyHandler -Chord Ctrl+k -Function KillLine
Set-PSReadlineKeyHandler -Chord Ctrl+u -Function BackwardKillLine
Set-PSReadlineKeyHandler -Chord Ctrl+w -Function BackwardKillWord
Set-PSReadlineKeyHandler -Chord Ctrl+Insert -Function Copy
Set-PSReadLineKeyHandler -Chord Tab -Function Complete
```

PowerShell の文法などにも触れつつ、順に見ていきたいと思います。

### Edit-Hosts

```powershell
function Edit-Hosts {
    Start-Process `
        -FilePath wsl.exe `
        -ArgumentList 'vim /mnt/c/Windows/System32/drivers/etc/hosts' `
        -Verb RunAs
}
```

まずは、`Edit-Hosts` です。  
こちらは名前解決で用いる `hosts` ファイルを編集するための関数で、PowerShell に元々存在する関数ではなく、独自に定義した関数になります。

関数を定義するには `function` キーワードを使用します。  
中括弧で囲まれた部分が実行されるコードです。

`Start-Process` はその名の通り指定したプロセスを起動するコマンドレット、`-Verb RunAs` は管理者権限で起動するという意味のオプションです。  
コマンドレット (Cmdlet) は PowerShell で利用可能なコマンドのことです。

`hosts` ファイルを書き換えて保存するためには管理者権限が必要になるため、WSL (Windows Subsystem for Linux) を管理者権限で起動し、WSL 上の Vim で `hosts` ファイルを開いています。

私は WSL のヘビーユーザーなのでこのようにしていますが、起動するエディタは好きなものを指定すればOKです。  
例えば、メモ帳を起動する場合は以下のようになります。

```powershell
Start-Process `
    -FilePath notepad.exe `
    -ArgumentList 'C:\Windows\System32\drivers\etc\hosts' `
    -Verb RunAs
```

この例のように、コマンドの途中で改行する場合は行の末尾にバッククォート (`) を付けます。

ちなみに、PowerShell のコマンドレットや関数は基本的に「<動詞>-<名詞>」という名前付けになっており、前半の部分は使える動詞があらかじめ決められています。  
使用が承認された動詞の一覧を取得するには、`Get-Verb` 関数を実行します。

```
PS C:\Users\xxx\Documents\WindowsPowerShell > Get-Verb

Verb        Group
----        -----
Add         Common
Clear       Common
Close       Common
Copy        Common
Enter       Common
Exit        Common
Find        Common
Format      Common
Get         Common
Hide        Common
Join        Common
Lock        Common
Move        Common
New         Common
Open        Common
       ︙
```

### Get-EnvironmentVariables

```powershell
function Get-EnvironmentVariables {
    [Environment]::GetEnvironmentVariables().GetEnumerator() | Sort-Object -Property Key
}
```

次は `Get-EnvironmentVariables` です。  
こちらは Windows で設定されている環境変数を一覧で表示する関数になります。

`[Environment]::GetEnvironmentVariables()` の部分で .NET Framework の `Environment.GetEnvironmentVariables` メソッドを呼び出しています。  
このように .NET Framework の豊富なAPIを利用できるのは、PowerShell の大きな強みの1つです。  
さらに、PowerShell はオブジェクト指向に基づいており、コマンドの出力は .NET のオブジェクトとして表現され、そのままパイプラインで別のコマンドの入力として渡すことができます。

`[Environment]::GetEnvironmentVariables()` の戻り値の型は `Hashtable` で、そのままでも整形して見やすいフォーマットで表示してくれますが、環境変数名がアルファベット順に並んでおらず探しにくいです。

```
PS C:\Users\xxx\Documents\WindowsPowerShell > [Environment]::GetEnvironmentVariables().GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Hashtable                                System.Object


PS C:\Users\xxx\Documents\WindowsPowerShell > [Environment]::GetEnvironmentVariables()

Name                           Value
----                           -----
SESSIONNAME                    Console
ProgramFiles(x86)              C:\Program Files (x86)
ProgramW6432                   C:\Program Files
PROCESSOR_IDENTIFIER           Intel64 Family 6 Model 167 Stepping 1, GenuineIntel
TMP                            C:\Users\xxx\AppData\Local\Temp
                            ︙
```

そこで、`GetEnumerator()` メソッドで Enumerator に変換したうえで `Sort-Object` コマンドレットで名前順にソートして表示しています。

```
PS C:\Users\xxx\Documents\WindowsPowerShell > Get-EnvironmentVariables

Name                           Value
----                           -----
ALLUSERSPROFILE                C:\ProgramData
ANDROID_HOME                   C:\Users\xxx\AppData\Local\Android\Sdk
APPDATA                        C:\Users\xxx\AppData\Roaming
COMMONPROGRAMFILES             C:\Program Files\Common Files
CommonProgramFiles(x86)        C:\Program Files (x86)\Common Files
                            ︙
```

### Edit-EnvironmentVariables

```powershell
function Edit-EnvironmentVariables {
    Start-Process `
        -FilePath rundll32.exe `
        -ArgumentList 'sysdm.cpl,EditEnvironmentVariables' `
        -Verb RunAs
}
```

次は `Edit-EnvironmentVariables` です。  
この関数を実行すると、Windows の環境変数のウィンドウを直接開くことができます。

![2024-04-07 20_55_38-環境変数.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3765253/15ff40ec-668a-1d06-0ee0-36ab89b6fafa.png)

こちらもシステム環境変数を編集するためには管理者権限で起動する必要があり、`-Verb RunAs` オプションを付加しています。

### Export-JavaHome

```powershell
function Export-JavaHome ([int] $version) {
    switch ($version) {
        8 {
            $dir = 'C:\langs\java\jdk8u312-b07'
        }
        11 {
            $dir = 'C:\langs\java\jdk-11.0.14.101-hotspot'
        }
        17 {
            $dir = 'C:\langs\java\jdk-17.0.2.8-hotspot'
        }
        default {
            Write-Host 'Error: Invalid Java version'
            return
        }
    }
    Start-Process `
        -FilePath powershell.exe `
        -ArgumentList "-Command [Environment]::SetEnvironmentVariable('JAVA_HOME', '${dir}', [EnvironmentVariableTarget]::Machine)" `
        -Verb RunAs
}
```

次は `Export-JavaHome` です。  
こちらは `JAVA_HOME` 環境変数を書き換えることで使用する Java のバージョンを切り替える関数になります。

WSL では [SDKMAN!](https://sdkman.io/) という JVM 系のソフトウェアのバージョン管理ができるコマンドラインツールを使用しているのですが、Windows には対応していないため PowerShell スクリプトで実現しています。

これまでの関数と異なる点として、関数名の後に括弧を置き引数を定義しています。  
PowerShell では、PHP のように変数名の頭に `$` を付けます。

この関数では `int` 型の引数を受け取り、`switch` ステートメントで引数の値によって `JAVA_HOME` に設定する値 (ディレクトリのパス) を切り替えています。  
引数が 8, 11, 17 のいずれでもない場合はエラーメッセージを表示して処理を中断します。

```
PS C:\Users\xxx\Documents\WindowsPowerShell > Export-JavaHome 10
Error: Invalid Java version
```

なお、`int` 型に変換できない値を引数に指定して実行するとエラーになります。

```
PS C:\Users\xxx\Documents\WindowsPowerShell > Export-JavaHome a
Export-JavaHome : パラメーター 'version' の引数変換を処理できません。値 "a" を型 "System.Int32" に変換できません。エラー: "入力文字列の形式が正しくありません。"
発生場所 行:1 文字:17
+ Export-JavaHome a
+                 ~
    + CategoryInfo          : InvalidData: (:) [Export-JavaHome]、ParameterBindingArgumentTransformationException
    + FullyQualifiedErrorId : ParameterArgumentTransformationError,Export-JavaHome
```

### プロンプトの設定

```powershell
function prompt {
    $principal = [Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()
    if ($principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
        $role = 'Admin'
    } else {
        $role = 'User'
    }
    '[{0}] {1} > ' -f $role, ((Get-Location).Path -split '::')[-1]
}
```

続いての `prompt` 関数は元々定義されている特殊な関数で、この関数の戻り値が PowerShell のプロンプトとして表示されます。(Bash でいうところの `PS1` 環境変数に相当します。)

私は WSL 上で Windows PowerShell を起動することが多いのですが、デフォルトの設定だと以下のようにプロンプトがかなり長くなり見づらいです。

```
$ powershell.exe
Windows PowerShell
Copyright (C) Microsoft Corporation. All rights reserved.

新しいクロスプラットフォームの PowerShell をお試しください https://aka.ms/pscore6

PS Microsoft.PowerShell.Core\FileSystem::\\wsl$\Ubuntu\home\xxx\pwsh> Get-Location

Path
----
Microsoft.PowerShell.Core\FileSystem::\\wsl$\Ubuntu\home\xxx\pwsh
```

そのため、ディレクトリのパスに `::` が含まれる場合は `::` 以降を表示するようにしています。

また、.NET の `Security.Principal` 名前空間のクラスを使用して PowerShell が管理者権限で起動しているかどうかを判定し、プロンプトで判断できるようにしています。

以下は管理者権限で起動した場合のプロンプトです。
```
[Admin] C:\Windows\system32 >
```
関数の最終行の `-f` は Format 演算子で、左辺の書式文字列に右辺の値を埋め込みます。  
また、PowerShell の関数では `return` キーワードを使用しなくても関数内で出力した値が呼び出し元に返却されるため、`return` キーワードは省略しています。

### エイリアスの設定

```powershell
Set-Alias hosts Edit-Hosts
Set-Alias env Edit-EnvironmentVariables
```

続いて、`Set-Alias` コマンドレットを使用して関数のエイリアスの設定を行っています。  
この設定により、`Edit-Hosts` と入力する代わりに `hosts` と入力すれば管理者権限の Vim が起動します。

PowerShell のコマンドレットは文字数が多いため、デフォルトで設定されているエイリアスも多数存在します。  
エイリアスの一覧を取得するには、`Get-Alias` コマンドレットを実行します。

```
PS C:\Users\xxx\Documents\WindowsPowerShell > Get-Alias

CommandType     Name                                               Version    Source
-----------     ----                                               -------    ------
Alias           % -> ForEach-Object
Alias           ? -> Where-Object
Alias           ac -> Add-Content
Alias           asnp -> Add-PSSnapin
Alias           cat -> Get-Content
Alias           cd -> Set-Location
Alias           CFS -> ConvertFrom-String                          3.1.0.0    Microsoft.PowerShell.Utility
Alias           chdir -> Set-Location
Alias           clc -> Clear-Content
Alias           clear -> Clear-Host
                    ︙
```

最初に入力した `cd` コマンドも `Set-Location` コマンドレットのエイリアスであることが分かります。  
他にも UNIX のコマンド名がエイリアスに設定されているものがたくさんあるので、コマンドレット名が分からない時はとりあえず UNIX コマンドをタイプしてみるとよいかもしれません。

### キーバインドの設定

```powershell
Set-PSReadlineKeyHandler -Chord Alt+b -Function BackwardWord
Set-PSReadlineKeyHandler -Chord Alt+d -Function KillWord
Set-PSReadlineKeyHandler -Chord Alt+f -Function NextWord
Set-PSReadlineKeyHandler -Chord Ctrl+a -Function BeginningOfLine
Set-PSReadlineKeyHandler -Chord Ctrl+d -Function DeleteCharOrExit
Set-PSReadlineKeyHandler -Chord Ctrl+e -Function EndOfLine
Set-PSReadlineKeyHandler -Chord Ctrl+k -Function KillLine
Set-PSReadlineKeyHandler -Chord Ctrl+u -Function BackwardKillLine
Set-PSReadlineKeyHandler -Chord Ctrl+w -Function BackwardKillWord
Set-PSReadlineKeyHandler -Chord Ctrl+Insert -Function Copy
Set-PSReadLineKeyHandler -Chord Tab -Function Complete
```

次に、`PSReadLine` モジュールの `Set-PSReadLineKeyHandler` コマンドレットを使用してキーバインドの設定を行っています。

`PSReadLine` モジュールがインストールされていない場合は以下のコマンドでインストールします。
```
Install-Module -Name PSReadLine -Force
```
私は Bash のキーバインドに慣れているので、[Tab] で補完、[Ctrl] + [d] で exit など、できるだけ Bash のキーバインドに近付けるように設定をしています。  
個人的には、この設定をするかどうかで PowerShell の使いやすさ・快適さが大きく異なってくると感じます。

## その他

プロファイルでの設定は不要ですが、[gsudo](https://gerardog.github.io/gsudo/) のインストールをおすすめします。

`sudo` コマンドのようにコマンドを一度だけ Windows 管理者権限で実行したり、
`su` コマンドのように現在のセッションを管理者権限に昇格したりすることができます。

WSL 上でも利用可能です。

```
$ gsudo powershell.exe
Windows PowerShell
Copyright (C) Microsoft Corporation. All rights reserved.

新しいクロスプラットフォームの PowerShell をお試しください https://aka.ms/pscore6

[Admin] C:\Users\morita\Documents\WindowsPowerShell > exit

$ powershell.exe
Windows PowerShell
Copyright (C) Microsoft Corporation. All rights reserved.

新しいクロスプラットフォームの PowerShell をお試しください https://aka.ms/pscore6

[User] C:\Users\morita\Documents\WindowsPowerShell > gsudo
[Admin] C:\Users\morita\Documents\WindowsPowerShell > exit
[User] C:\Users\morita\Documents\WindowsPowerShell >
```

## 主な参考文献

- PowerShell とは - PowerShell | Microsoft Docs  
    https://docs.microsoft.com/ja-jp/powershell/scripting/overview
- 実行ポリシーについて - PowerShell | Microsoft Docs  
    https://docs.microsoft.com/ja-jp/powershell/module/microsoft.powershell.core/about/about_execution_policies
- プロファイルについて - PowerShell | Microsoft Docs  
    https://docs.microsoft.com/ja-jp/powershell/module/microsoft.powershell.core/about/about_profiles
- プロンプトについて - PowerShell | Microsoft Docs  
    https://docs.microsoft.com/ja-jp/powershell/module/microsoft.powershell.core/about/about_prompts
- 演算子について - PowerShell | Microsoft Docs  
    https://docs.microsoft.com/ja-jp/powershell/module/microsoft.powershell.core/about/about_operators
- PSReadLine について - PowerShell | Microsoft Docs  
    https://docs.microsoft.com/ja-jp/powershell/module/psreadline/about/about_psreadline