🐚
【PowerShell】対話形式でテンプレートから定型文を生成する
こちらの CLI に憧れて自分でもひとつツールを書いてみました。
環境:
> $PSVersionTable
Name Value
---- -----
PSVersion 7.2.4
PSEdition Core
GitCommitId 7.2.4
OS Microsoft Windows 10.0.19044
Platform Win32NT
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1
WSManStackVersion 3.0
$PSStyle
で出力に色をつけているのでバージョン7以降が必要です。
作ったもの
template.txt
昔、あるところに{{太郎}}がいました。
{{太郎}}は大きくなったら{{島}}に行って{{ターゲット|鬼;悪魔}}を退治するといい、{{お供}}を連れて旅立ちました。
このようなテンプレートを書いて、Invoke-Snippet .\template.txt
と呼び出してしてやると…
こんな感じで入力欄をプレビュー表示してから、
各項目について入力プロンプトを出します。
複数選択の場合は番号で指定します。
ここで「その他」(-1
)を入力するか何も回答せずに ENTER を押すと、あらためて自由回答欄を表示します。
全部入力したらテンプレートのプレースホルダーを置き換えた文字列がクリップボードにコピーされるようになっています。
コード
PsInteract
というクラスを作ってみました。
コード全体
クラスや関数の内部から $PSStyle
を使うときは $Global:PSStyle
のようにグローバルスコープ変数としてやる必要があります。
class PsInteract {
[string[]]$Template = @()
[string[]]$Preview = @()
[PSCustomObject[]]$Questions = @()
[System.Collections.Specialized.OrderedDictionary]$ReplaceMap = @{}
# コンストラクタ
PsInteract($templatePath) {
$this.Template = Get-Content $templatePath
[regex]::Matches($this.Template, "{{.+?}}").Value | ForEach-Object {
if ($_ -notin $this.ReplaceMap.Keys) {
$this.ReplaceMap.Add($_, "") # まずは空文字を割り当てておく
}
}
$p = $this.Template
$this.ReplaceMap.Keys | ForEach-Object {
$p = $p.replace(
$_,
($Global:PSStyle.Background.BrightBlue + $Global:PSStyle.Foreground.Black + ( "[{0}]" -f [PsInteract]::ParseQuestion($_).Prompt ) + $Global:PSStyle.Reset)
)
}
$this.Preview = $p
$this.Questions = $this.ReplaceMap.Keys | ForEach-Object { [PsInteract]::ParseQuestion($_) }
}
# プレースホルダーを読み取って質問オブジェクトを生成する静的メソッド
static [PSCustomObject] ParseQuestion([string]$q) {
$content = $q.Trim("{}")
if ($content.IndexOf("|") -ne -1) {
return [PSCustomObject]@{
"PlaceHolder" = $q;
"FreeInput" = $false;
"Prompt" = ($content.Split("|"))[0];
"Options" = @(($content.split("|"))[1] -split "[;;]");
}
}
return [PSCustomObject]@{
"PlaceHolder" = $q;
"FreeInput" = $true;
"Prompt" = $content;
"Options" = @();
}
}
# 複数選択の場合の選択肢を表示する形式に整形する静的メソッド
static [string[]] FormatOptions([string[]]$options) {
$idx = 1
$opts = @()
$options | ForEach-Object {
$opts += "[{0,2}] {1}" -f $idx, $_.Trim()
$idx += 1
}
$opts += "[-1] その他"
return @($opts).ForEach({" {0}" -f $_})
}
[void] Ask() {
$this.Questions | ForEach-Object {
$userInput = ""
$prompt = $Global:PSStyle.Foreground.BrightBlue + "`u{00A7}" + $_.Prompt + $Global:PSStyle.Reset
if ($_.FreeInput) {
$userInput = Read-Host -Prompt $prompt
}
else {
$prompt | Write-Host
[PsInteract]::FormatOptions($_.Options) | Write-Host
$choice = (Read-Host -Prompt "Choice") -as [int]
if ($choice -le 0) {
$userInput = Read-Host -Prompt "==>"
}
else {
$userInput = $_.Options[($choice - 1)]
}
}
$this.ReplaceMap[$_.PlaceHolder] = $userInput
}
}
[string[]] Apply() {
$s = $this.Template
$this.ReplaceMap.GetEnumerator() | ForEach-Object {
$s = $s.replace($_.key, $_.value)
}
return $s
}
}
下記のようにプロパティを用意しています。
-
Template
:指定したテンプレートを格納する文字列配列 -
Preview
:プレビュー表示用の文字列配列 -
Questions
:プレースホルダーから生成した質問を格納するカスタムオブジェクトの配列 -
ReplaceMap
:質問とそれに対する回答の対応表-
@{}
で宣言した PowerShell の連想配列は順番を保持しません。System.Collections.Specialized.OrderedDictionary
の型を指定してやることで投入した順番に格納されます。 - 代わりに
[ordered]@{}
と初期化するのでも同じことができます。
-
.Ask()
で質問を開始して、.Apply()
で回答内容をテンプレートに反映させます。
呼び出し側
function Invoke-Snippet {
param (
$templatePath
)
$a = [PsInteract]::new($templatePath)
"========================================" | Write-Host
$a.Preview | Write-Host
"========================================" | Write-Host
$a.Ask()
$a.Apply() | Set-Clipboard
"Filled fields and copied to clipboard!" | Write-Host -ForegroundColor Yellow
}
最近ようやくクラスの使い方が見えてきたような気がしています。
Discussion