シェルスクリプト&PowerShell Advent Calendar 2024の記事です。
はじめて外部のアドベントカレンダーに参加させていただきました!
至らないところ等あるかと思いますがよろしくお願いします!
はじめに
この記事は PowerShell で自分で定義した(Powershell にもとからあるもの以外) コマンドやエイリアスの一覧を取得するためのものです。
皆さん自分好みにシェルをカスタマイズしているかと思いますが、過去に自分が登録したものって忘れてしまいますよね..? でもいちいち $PROFILE
を開くのも面倒だし...
そんな方にぴったりだと思います。
本記事では2通りのやり方で実現します。
-
$PROFILE
の実行前と実行後を比較する方法- 面倒ですが確実です
-
FunctionInfo.ScriptBlock.File
とAliasInfo.HelpUri
を用いる方法- 少し確率的な方法になってしまいます
基本の方針
基本は
- Alias を取得するには
Get-Alias
を、 - 自作コマンドを取得するには
Get-ChildItem Function:
を用います。
後者に関しては以下の記事がとても参考になるのでそちらをどうぞ。
簡単に言えば Powershell ではコマンドも変数もドライブ(C:
とかと同じヤツ)に入るのでそこを探すのです。ドライブについては以下の公式ドキュメントをどうぞ。
作成するコマンドのインタフェース
以下のようなユーザインタフェースにします。
- 引数なしの場合: コマンドとエイリアスを一覧表示
# 引数を与えない場合: コマンドとエイリアスを一覧表示
> Get-UserDefined
function:
Name SourceFile Definition
---- ---------- ----------
Add-PoshGitToProfile D:\Scoop\modules\posh-git\Utils.ps1 ...
ConvertTo-MsysPath D:\PowershellPrograms\PowershellCmdlets\ConvertTo-msysPath.ps1 ...
ConvertTo-Narrow D:\PowershellPrograms\PowershellCmdlets\ConvertCharWidth.ps1 ...
ConvertTo-Slash C:\Users\XXX\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1 ...
ConvertTo-Wide D:\PowershellPrograms\PowershellCmdlets\ConvertCharWidth.ps1 ...
Copy-Date C:\Users\XXX\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1 ...
cpwd C:\Users\XXX\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1 ...
alias:
Name Definition
---- ----------
code code-insiders
cpdt Copy-Date
ctmsys ConvertTo-MsysPath
Rgui C:\Program Files\R\R-4.3.0\bin\x64\Rgui.exe
- 引数ありの場合: 情報を詳細表示
# コマンド
> Get-UserDefined copy-date
function:
Name:
Copy-Date
SourceFile:
C:\Users\XXX\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1
Definition:
param(
[validateset("today", "yesterday", "tomorrow")]$when = "today",
[validateset(
"yyyyMMdd",
"yyyy/MM/dd",
"yyyy-MM-dd",
"normal",
...
# alias
> Get-UserDefined rgui
alias:
Name:
Rgui
Definition:
C:\Program Files\R\R-4.3.0\bin\x64\Rgui.exe
$PROFILE
の実行前と実行後を比較する方法
この方法は $PROFILE
を実行する前と後でオブジェクトを採取し、それを比較する方法です。
まずは以下のスクリプトを $PROFILE
(およびそれに準ずるもの) の 先頭に 置きます
# default aliases and functions
$defaultAliases = (Get-Alias)
$defaultFuncs = (Get-ChildItem Function:)
Write-Output "`nIf you see all user-defined functions or aliases, type:`n`tGet-UserDefined`n"
次に、以下のコマンドを先ほどのファイルの 最後に 置きます。
function Get-UserDefined {
param (
[string]$name
)
$allAliases = (Get-Alias)
$allFuncs = (Get-ChildItem Function:)
# 差分を取得する
$aliasDiff = (Compare-Object $defaultAliases $allAliases).InputObject
$funcDiff = (Compare-Object $defaultFuncs $allFuncs).InputObject
# 表示しやすいように PSCustomObject にする
$userAliases = $aliasDiff | ForEach-Object {
[PSCustomObject]@{
Name = $_.Name
Definition = $_.Definition
}
}
$userFuncs = $funcDiff | ForEach-Object {
[PSCustomObject]@{
Name = $_.Name
SourceFile = $_.ScriptBlock.File
Definition = $_.Definition
}
}
# もし引数が与えられていた場合の処理
if ($name) {
$aliasDiff | Where-Object {
$_.Name -eq $name
} | ForEach-Object {
Write-Output "`n`nalias:"
Write-Output "Name:"
Write-Output "`t$($_.Name)"
Write-Output "`nDefinition:"
Write-Output "`t$($_.Definition)"
}
$funcDiff | Where-Object {
$_.Name -eq $name
} | ForEach-Object {
Write-Output "`n`nfunction:"
Write-Output "Name:"
Write-Output "`t$($_.Name)"
Write-Output "`nSourceFile:"
Write-Output "`t$($_.ScriptBlock.File)"
Write-Output "`nDefinition:"
Write-Output $_.Definition
}
}
# 引数なし
else {
Write-Output "`nfunction:"
Write-Output $userFuncs | Format-Table -Property Name, SourceFile, Definition
Write-Output "`nalias:"
Write-Output $userAliases | Format-Table -Property Name, Definition
}
}
これで上記2つの間で定義された alias やコマンドを参照できるというわけです。
しかしこれでは場所に依存してしまいますし、あまり便利とは言えません。まあ間にあるものは確実にキャプチャできるので確実と言えば確実ですが。
FunctionInfo.ScriptBlock.File
と AliasInfo.HelpUri
を用いる方法
そこで次に確実ではないものの、不便でもない方法を紹介します。
先ほどと同じように Get-ChildItem Function:
と Get-Alias
で情報を取得するのですが、それを以下のようにして選別します。
- (コマンド)
FunctionInfo.ScriptBlock.File
を使って、コマンドが定義されたファイルを調べる- これは確実と言えば確実です。本記事では元からあるものとモジュール以外のすべてをユーザ定義コマンドとして扱いました。これは人によって変えてもいいと思います。
- (alias)
AliasInfo.HelpUri
を使ってコマンドのヘルプの参照先として指定されている URL で選別する- 問題はこっちで、この
HelpUri
はヘルプドキュメントの.LINK
で指定できるものです。これはユーザが好きに定義できます。今回ではマイクロソフト公式のURLを指定するものを除外するという方法を取りましたが、確実ではありません。 - ヘルプドキュメントについては: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_comment_based_help
- 問題はこっちで、この
前者に関してはもっといい方法が無いかと調べているときに、以下の先駆者様を発見し、参考にさせていただきました。
コマンド:
function Get-UserDefined2 {
param (
[string]$name
)
# ユーザalias
$userAliases = (Get-Alias) | Where-Object {
$isUserAlias = $true
if ($_.HelpUri) {
# 元からある alias は Help Link が以下の2つ(多分)
# ただしユーザであっても Help ドキュメントに以下のURLを記載することは可能なため万能ではない
if ($_.HelpUri -like "https://go.microsoft.com/fwlink*" -or $_.HelpUri -like "https://learn.microsoft.com/powershell/*"){
$isUserAlias = $false
}
}
Write-Output $isUserAlias
} | ForEach-Object {
[PSCustomObject]@{
Name = $_.Name
Definition = $_.Definition
}
}
# ユーザコマンド
$userFuncs = (Get-ChildItem Function:) | Where-Object {
$isUserFunc = $true
$filepath = $_.ScriptBlock.File
if ($filepath) {
# ScriptBlock.File の値が空でないもの
foreach ($path in $env:PSModulePath -split ";") {
# module だったらユーザコマンドではない(と考える)
if ($filepath -like "${path}*") { $isUserFunc = $false }
}
}
else {
# ScriptBlock.File の値が空だったら元からあるもの
$isUserFunc = $false
}
Write-Output $isUserFunc
} | ForEach-Object {
[PSCustomObject]@{
Name = $_.Name
SourceFile = $_.ScriptBlock.File
Definition = $_.Definition
}
}
# もし引数が与えられていた場合の処理
if ($name) {
$userAliases | Where-Object {
$_.Name -eq $name
} | ForEach-Object {
Write-Output "`n`nalias:"
Write-Output "Name:"
Write-Output "`t$($_.Name)"
Write-Output "`nDefinition:"
Write-Output "`t$($_.Definition)"
}
$userFuncs | Where-Object {
$_.Name -eq $name
} | ForEach-Object {
Write-Output "`n`nfunction:"
Write-Output "Name:"
Write-Output "`t$($_.Name)"
Write-Output "`nSourceFile:"
Write-Output "`t$($_.SourceFile)"
Write-Output "`nDefinition:"
Write-Output $_.Definition
}
}
# 引数なし
else {
Write-Output "`nfunction:"
Write-Output $userFuncs | Format-Table -Property Name, SourceFile, Definition
Write-Output "`nalias:"
Write-Output $userAliases | Format-Table -Property Name, Definition
}
}
終わりに
結構需要が高そうなことで、先駆者様などいろいろな人が工夫していることがわかりました。
もっといい方法ないかなぁ...