PowerShell バッチ用待機スクリプト

Windows でダブルクリックでプログラムを実行したいと思ったら、 終了時の待機処理が欲しかったりしますよね。

タイトル通り PowerShell の利用を前提としています。 具体的には PowerShell 7 です。

å‹•æ©Ÿ

さて。

何かのプログラミング言語でコードを書いたとき、 そこで待機処理を書いてもいいが、 例えば python とかだと py main.py みたいにしないといけない訳で。

デフォルトが bash 環境ならそれでも問題ないが、 Windows ユーザーだと、 わざわざシェルを立ち上げてから実行する手間は面倒。

だから、 .bat なり .ps1 なりを叩いたら (=ダブルクリックしたら) 実行できるようにしたい。

bat はコードがゴミ *1 なので、 PowerShell を使う。

加えて、 python の rich とかを使って from rich.traceback import install なんかを使うと楽にエラー終了処理を書けるが、 例外を投げたまま終了させないといけないので、 python 中でエラーハンドリングができない。 (ここは多分知らないだけでできるとは思う)

更に、ダブルクリックで起動した処理は、 何もしなければ実行後一瞬で落ちてしまう。 ログは勿論異常終了のエラー内容も見れないので、 それも防ぎたい。 (ログファイルに出してもいいけど、わざわざ開くのも手間)

要件

ということで。

  • ダブルクリックでプログラムを実行したい。
  • プログラムが例外を出したとき、エラーハンドリングせず異常終了させたい。
  • それを呼び出し側でそれを受け付けて処理をしたい。
  • 処理終了時 (特にエラー終了時) にすぐに終了させないようにしたい。
    • 例えばキー押下等を機に終了させたい。

スクリプト

これを実行するための インポート用 PowerShell スクリプトを書いた、 もとい、毎回思い出して書き直すのが面倒なので、 ここにまとめておく。

util.ps1

インポート用 PowerShell スクリプトとして、 util.ps1 を以下のように書く。

#!/usr/bin/env pwsh

# ユーザー設定変数
$__COUNT_STOP_OK__ = 5  # 成功時の待機時間 (秒)
$__COUNT_STOP_NG__ = 180 # 失敗時の待機時間 (秒)
$__PROMPT_INTERVAL__ = 0.1 # 待機中のキー押下待受間隔 (秒)
$__LAST_WAIT_TIME__ = 0.2 # 全終了完了時に一瞬待機させる秒数

# 実行後待ち受け処理
function waitForKeyPress($ret) {
    # 計算用変数
    $enableProceed = $true # 一定時間ごとに "." と催促する表示の有無。
    $countPerSec = [math]::Truncate(1.0 / $__PROMPT_INTERVAL__) + 1
    $countStop = $__COUNT_STOP_OK__ * $countPerSec
    
    # 終了時のメッセージ出力、及び、変数の設定
    if ($ret) {
        Write-Host "-----"
        Write-Host "Completed." -ForegroundColor "Green"
    } else {
        Write-Host "-----"
        Write-Host "Error." -ForegroundColor "Red"
        $countStop = $__COUNT_STOP_NG__ * $countPerSec
        $enableProceed = $false
    }
    
    # キー押下 or タイムアウトまで待機する処理
    $count = 0
    Write-Host 'Press any key.' -ForegroundColor "Red"
    while ($true) { 
        # キーを押下したら終了
        if ([Console]::KeyAvailable) {
            $keyInfo = [Console]::ReadKey()
            if ($keyinfo.Key) {
                Write-Host 'Terminated.' -ForegroundColor "Red"
                break
            }
        }
    
        # 指定した秒数に達したら終了。
        Start-Sleep($__PROMPT_INTERVAL__ = 0.1 # 待機中のキー押下待受間隔 (秒)
        )
        if ($count -ge $countStop) {
            Write-Host
            Write-Host "Timeout." -ForegroundColor "Red"
            break
        }
        if ($enableProceed) {
            # 1秒ごとにプロンプトを表示。
            if ($count % $countPerSec -eq 0) {
                Write-Host "." -ForegroundColor "Red"
            }
        }
        $count += 1
    }
    Start-Sleep($__LAST_WAIT_TIME__)
}

ありがちな変数を使っているので、 かぶる場合は変数名を変更すること。

呼び出し側

util.ps1 を呼び出すときは以下のようにする。

#!/usr/bin/env pwsh

. ./util.ps1

# 実行コード
py ./main.py # 適宜変更

waitForKeyPress $?

実行コードは function に入れてもいい (と思う) が、 python だとシェルを引き継がない。 なので、 rich 等の色付き文字が無効化される。

実行コードを別に書いておいて Invoke-Expression なんかする方法もあるが、 実行コード側の終了コードを受け取れない。 そうすると、 $? は常に True になってしまう。

というわけで、それを防ぐには、地の文で実行するしかない。 (もとい、それしか方法をしらない)

動作概要

挙動

正常終了時は、 $countStopOK で設定された秒数だけ待機するか、 キー押下時に終了する。 このとき、1秒ごとに催促するプロンプト (.) が表示される。

異常終了時もキー押下時に終了するが、 $countStopNG を大きく取ることで、 エラー出力を確認する時間を設ける。 こちらもキー押下で終了できる。 エラー確認中にスクロールされるのを防ぐために、 プロンプトは表示しない。

動作概要

特筆すべきと思われる点について補足しておく。

  • キー押下は $promptInterval で指定した秒数ごとに確認される。
    • リアルタイムにキー入力を受け付けるのではない。
    • なので、適当な値に設定する。
      • 長すぎるとキー押下に反応しない。
      • 短すぎると処理負荷がかかりそう。
  • $countStopNG は適宜変更すること。
    • 正常終了時は短くていい (上記では 5秒)。
    • 異常終了時は長めに生き残っていてほしい (上記では 3分)。
    • 特に、バッチ処理だと放りっぱなしにしておくことも多いと思うので、 十分大きな値にする。
      • 1時間なら 3600 秒。
      • 1日なら 86400 秒。

おわりに

ちなみに bash が使えるなら、待受処理部分は、

$timeoutSec = 10
bash -c "read -n 1 -t $timeoutSec -p 'Press any key:'"

って書けます。

今までの苦労は(ry *2

bash 利用版

#!/usr/bin/env pwsh

# ユーザー設定変数
$__COUNT_STOP_OK__ = 5  # 成功時の待機時間 (秒)
$__COUNT_STOP_NG__ = 180 # 失敗時の待機時間 (秒)
$__LAST_WAIT_TIME__ = 0.2 # 全終了完了時に一瞬待機させる秒数

# 実行後待ち受け処理
function waitForKeyPress($ret) {
    # 終了時のメッセージ出力、及び、変数の設定
    if ($ret) {
        Write-Host "-----"
        Write-Host "Completed." -ForegroundColor "Green"
        $timeoutSec = $__COUNT_STOP_OK__
    } else {
        Write-Host "-----"
        Write-Host "Error." -ForegroundColor "Red"
        $timeoutSec = $__COUNT_STOP_NG__
    }
    
    # キー押下 or タイムアウトまで待機する処理
    bash -c "read -n 1 -t $timeoutSec -p 'Press any key:'"
    Start-Sleep($__LAST_WAIT_TIME__)
}

どう考えてもこっちを使うべきでは?

PowerShell だけでやらないといけない理由があったはずなんだけどなぁ。

*1:控えめに言ってもゴミ。

*2:実際、 PowerShell が使えて bash 使えない環境はあまり想像できないし、 本当に何のためか分からない。