🛠️

[HTA代替]PowerShellを実行できるローカルHTML製のツールを作る

2025/03/02に公開

PowerShellを実行する社内ツールをHTA(HTMLアプリケーション)で作っていたが、さすがに2025年にIEベースでHTMLを書くのに限界を感じたので、Edge/Chromeへの移行をチャレンジした
結論としては、カスタムURLとjsファイル読込を駆使することで、おおむねやりたいこと(HTA相当のことをEdgeで行う)が実現することができた

先行事例として次期HTAとしてのPowerShell+WebView2の利用があり、とても参考になったが、今回はWebView2ランタイムを不要とする方法を追求した

実現したいこと

以下の要件を満たすツールを作りたい

  • HTA相当のことを、画面をHTML5で作って実現したい
  • オフラインで、追加のインストール不要で動く
  • 画面操作からPowerShellが実行できる、PowerShellの結果を画面に表示できる
  • ツールのインストーラーが不要で、ファイルをコピーするだけで動く
  • コンパイル不要で、ソースが直接編集できる
  • 管理者権限不要
  • Windows11のデフォルト設定で動く(FWの穴あけ等が不要)
  • WebView2ランタイム不要

実現方法概要

  • ツールの起動はPowershellから行う
    • このシェルは、①画面から実行されるPowershellをカスタムURLに登録 と ②htmlファイルをEdgeで起動する を実行する
  • 画面はHTMLで作成したものをEdgeで表示する
  • 画面操作からPowerShellの実行は、登録したカスタムURLにアクセスすることで行う
  • PowerShellの実行結果を画面に表示する処理は、PowerShellの実行結果をjavascript形式でreturn.jsというローカルファイルに保存し、そのjsファイルを画面で読み込むことで実現する

サンプルファイル

全体構成

ツールは以下のファイルで構成される(すべて同じフォルダに置く)

Register-UrlScheme.ps1
index.html
myapp.ps1
return.js
  • Register-UrlScheme.ps1:ツール起動用のシェル
    • カスタムURLへのmyapp.ps1の登録と、index.htmlの起動を行う
  • index.html:画面表示用のHTML
    • ツール起動用のシェルから起動される
    • PowerShellを呼び出したいときは、カスタムURL(myapp://{id}/{value})にアクセスし、シェルの実行結果をローカルのjsファイル(return.js)経由で受け取る
  • myapp.ps1:画面処理用のシェル
    • カスタムURLから呼び出されるコマンドとして登録される
    • シェルの実行結果はreturn.jsに保存する
  • return.js:シェルの実行結果受け渡し用のjs
    • myapp.ps1から生成されて、index.htmlに読み込まれる
    • ファイルの中の処理で「globalShellReturn」という変数を更新することで、画面に情報を渡す

ツール起動用のシェル

Register-UrlScheme.ps1
# カスタムURLスキーム 'myapp://' を登録
$scheme = "myapp"
$command = """"+ $PSScriptRoot +"\myapp.ps1""" +" `"%1`""

$regPath = "HKCU:\Software\Classes\$scheme"
New-Item -Path $regPath -Force | Out-Null
Set-ItemProperty -Path $regPath -Name "(default)" -Value "URL:$scheme Protocol" -Force
Set-ItemProperty -Path $regPath -Name "URL Protocol" -Value "" -Force
$commandPath = "$regPath\shell\open\command"
New-Item -Path $commandPath -Force | Out-Null
Set-ItemProperty -Path $commandPath -Name "(default)" -Value "powershell.exe -ExecutionPolicy Bypass -File $command" -Force

# ツールの画面を起動
$html =""""+ $PSScriptRoot +"\index.html" + """"
Start-Process "msedge"  $html
  • ダブルクオーテーションがいっぱい並んでるのは、ファイルパスにスペースが含まれた場合の対策

画面用HTML

index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <script type="text/javascript">
      // グローバル変数 シェルの実行結果保存変数
      var globalShellReturn = {}

      // 実行ボタンの処理
      async function execute() {
        const id = new Date().getTime() // 現在時刻(ミリ秒)をidにする
        const value = encodeURIComponent("Hello")
        const url = `myapp://${id}/${value}`
        // powershell実行用のカスタムURLを呼び出し
        window.location.href = url

        // shellの完了を待機
        let returnValue = await waitShell(id)

        // shellの実行結果を画面に表示
        document.getElementById("result").innerHTML = returnValue
      }

      // グローバル変数を監視することでshellの実行完了まで待機し、完了したらシェルの戻り値を返す
      async function waitShell(id) {
        return new Promise((resolve) => {
          const check = () => {
            fileload()
            let value = globalShellReturn[id]
            if (value) {
              resolve(value)
            } else {
              setTimeout(check, 100)// 0.1秒単位でshellの完了をチェック
            }
          }
          check()
        })
      }

      // scriptタグを追加することでjsファイルを読み込む
      function fileload() {
        const el = document.createElement("script")
        el.src = "return.js"
        const sc = document.getElementById("forLoad")
        sc.innerHTML = ""
        sc.appendChild(el)
      }
    </script>
  </head>
  <body>
    <button onclick="execute()">実行</button>
    <p>実行結果:<span id="result"></span></p>
    <div id="forLoad"></div>
  </body>
</html>
  • return.jsに前の実行結果が残っていた場合の対策として、リクエスト単位でidを生成し、id指定でデータを読み込むようにしている

画面処理実行用のシェル

myapp.ps1
param ($url)

# URLデコード(%20 → スペース など)
$decodedUrl = [System.Uri]::UnescapeDataString($url)

# 出力ファイルのパス
$returnFilePath =$PSScriptRoot+"\return.js" 

# "myapp://xxx" の "xxx" 部分を抽出
if ($decodedUrl -match "myapp://(.+)") {
    $data = $matches[1]
    $parts = $data -split "/", 2

    # URLを解析してidと引数に分割
    $id = $parts[0]
    $input = $parts[1]
    
    # 例:引数に対応した処理
    $result = $id + "-" + $input + " World"

    # javascriptの変数を上書きする形で結果を保存
    $returnValue =  "globalShellReturn = { "+$id +": """ + $result +""" }"

    # 実行結果を.jsに保存
    Set-Content  -Path $returnFilePath -Value $returnValue
}

シェルの実行結果受け渡し用のjs

初期状態の中身は空でいい。値が入った場合は下記のような内容になる。

return.js
globalShellReturn = { 1740923674497: "1740923674497-Hello World" }

実行サンプル

①powershellで、ツール起動用のシェルを実行する

cmd
powershell .\Register-UrlScheme.ps1

②Edgeでツールの画面が表示される

③実行ボタンを選択すると、Powershellの実行結果が画面に表示される

補足

  • サンプルではHTMLとjsを同じファイルにしているが、別ファイルにすることも可能(画面はただのローカルHTML)
  • 画面から呼び出すシェルの内容が複数パターンある場合は、呼び出すURLで分岐すれば、1つのカスタムURL(と1つの登録用のシェルファイル)で実現可能
  • 1回カスタムURLを登録してしまえば、以降は、ローカルHTMLを直接立ち上げても動作する
  • シェルを複数並列で実行したい場合は、結果受け取り用に複数のjsファイルが必要
  • PowerShellで画面操作を行うことで、ローカルHTMLの画面からWeb画面を操作するツールを作れる

Discussion