ps1起動用バッチ作成ツールの開発メモ (PowerShell)

ps1を呼び出すバッチファイルを自動生成するPowerShellスクリプト製ツールを作成するにあたって分かったことや調べたことのメモ。

バッチ生成ツール

PowerShellスクリプト(.ps1)は設定を行わなければそもそもデフォルトで実行不可になっているうえ、バッチファイル(.bat)と異なりダブルクリックでの実行もできません。 実行が非常に不便なPowerShellスクリプトをどうにか[…]

色々細かい仕様で悩むことがあったので情報を共有。

(ちゃんと体裁を整えていないメモなので見づらいと思います…! ごめんなさい)

(以下、ドラッグ&ドロップはD&Dと略記します)

スクリプト内容の説明

コード本体 (D&Dで配布用バッチ生成_code.ps1)

# ==========関数定義部[始]==========
# 与えられたpathのPowerShellスクリプトの起動用バッチファイルを出力
function Out-BatchForDistribution($path) {
$psFileName = [System.IO.Path]::GetFileName($path)

$batchText = @"
@echo off
pushd %~dp0
powershell -ExecutionPolicy Bypass -File ".\$($psFileName)" %*
popd
"@

$batchFilePath = (Join-Path (Split-Path $path -Parent) ([System.IO.Path]::GetFileNameWithoutExtension($path))) + ".bat"
Set-Content -LiteralPath $batchFilePath -Value $batchText -Encoding Default
}

# 与えられたpathのフォルダ内にあるそれぞれのPowerShellスクリプトの起動用バッチファイルを出力
function Out-BatchesForDistribution($path) {
    Get-ChildItem -LiteralPath $path | ?{ $_.Name -like "*.ps1" } | %{ Out-BatchForDistribution $_.FullName }
}
# ==========関数定義部[終]==========

# ==========スクリプト実行開始点==========
if ($args.Length -eq 0) { exit }

# 引数で与えられたファイルパスがフォルダかファイルか判定し別関数を呼び出し。
# ファイルの場合は拡張子が.ps1の場合のみ別関数呼び出し。
foreach ($path in $args) {
    if (Test-Path -LiteralPath $path -PathType Container) {
        Out-BatchesForDistribution $path
    } else {
        if ([System.IO.Path]::GetExtension($path) -ne ".ps1") {continue}
        Out-BatchForDistribution $path
    }
}

CC0
Pajoca (パジョカ) はこのソースコードの全ての著作権および関連する権利をCC0により放棄しています。

流れ

  1. バッチにD&Dされた.ps1ファイルのパスが引数経由でこの.ps1スクリプトに渡される
  2. 受け取ったファイルパスがフォルダかファイルか判定し別関数を呼び出し
  3. 別関数で.ps1ファイルパスを相対パスで起動用バッチのコード本文に埋め込み
  4. ③で作ったコードを持つバッチファイルを生成

 

生成するバッチファイルの中身

@echo off
pushd %~dp0
powershell -ExecutionPolicy Bypass -File ".スクリプト名.ps1" %*
popd

CC0
Pajoca (パジョカ) はこのソースコードの全ての著作権および関連する権利をCC0により放棄しています。

処理概要

  • 邪魔な内容(バッチ側で実行したコマンド)がプロンプト画面に出力されないように @echo off
  • D&Dでバッチにファイルを渡すとカレントディレクトリが勝手に変わるので、バッチの存在場所に戻す pushd %~dp0
    ↑この挙動について詳しくは別記事で。
  • 指定名の.ps1スクリプトを相対パスでPowerShellに渡して起動。実行ポリシーを本スクリプトのみで昇格
    ↑詳しくは後述
  • ( pushdしたので popd )

 

バッチファイルの設計メモ(注意点)

.ps1からツールで生成されるバッチファイルの行数はたった4行。でもバッチの扱いに慣れていなくて地味につまづいた点がいくつかあったので詳細をこちらにまとめ。

バッチの仕様と罠

特定のPowerShellスクリプトを呼び出して実行するバッチファイルを作成していた時にハマった落とし穴をメモ。 [sitecard subtitle=バッチ生成ツール url=https://pajoca.com/ps1-batch-g[…]

要点をまとめると

  • バッチの文字コードは Shift_JIS で保存
  • D&Dでバッチにファイルを渡すと勝手にカレントディレクトリが変わる
  • バッチを管理者として実行するとカレントディレクトリが変わる
  • カレントディレクトリの問題は、バッチ本文で pushd %~dp0 すればOK

 

.ps1 ファイルのダブルクリック起動法の比較

主に、

  1. .ps1 ファイルを呼び出すバッチファイルをダブルクリック
  2. .ps1 ファイルのショートカット(PowerShellを呼び出すよう設定)をダブルクリック
  3. .ps1 ファイルを直接ダブルクリックして起動できるようWindows側の設定を変更

といった方法がある。

今回求めていた条件

  • ユーザー側で特別な設定や操作は不要
  • ファイルをドラッグ&ドロップしてバッチファイルに渡せる
  • 配布したスクリプトをどのディレクトリに置いても実行可能

を満たしたのは方法①だけだったので、バッチから呼び出す仕様にした。

(方法②はファイルのドラッグ&ドロップ渡しと任意ディレクトリでの実行が両立できず除外。方法③はユーザー側が設定を変えなければならず面倒ということで除外。)

方法②の、.ps1 ファイルのショートカット起動の問題点は以下に詳しくまとめたので必要に応じて参照。

関連記事

ショートカットからの.ps1起動について PowerShellのスクリプトファイル(.ps1)はダブルクリックで実行することができない。 代替法の一つとして「.ps1ファイルへのショートカットを作成し、そのショートカットでPowerSh[…]

スクリプト実行時に引数で指定したポリシーと、バッチ起動時の挙動

本ツールではバッチでPowerShellに.ps1ファイルの実行をさせる際、実行ポリシーBypass に指定した。

その理由について。

実行ポリシーとは

Powershellスクリプトの実行にどの程度セキュリティ上の制限をかけるかという設定。

例えば実行ポリシーがデフォルトの Restricted の場合、そもそもスクリプトが実行できない。(個別のコマンド入力のみ実行可能)

Windows本体の実行ポリシー設定の他に、特定のスクリプトを実行するときだけ(そのスクリプトに限り)一時的に別の実行ポリシーで実行することも可能。

本ツールではバッチから呼び出したPowerShellスクリプトを最も制限の緩い Bypass のポリシーで実行するようなコマンドにした。

つまり本体側の実行ポリシー設定は変えず、呼び出したスクリプトのみが一時的に Bypass のポリシーで実行される。

実行ポリシーについて、詳しくは公式ドキュメントで。

実行ポリシーの種類と動作

詳しくは上記ドキュメントで。主なポリシーとそのざっくり概要は以下の通り。

ポリシー
動作
Restricted
スクリプト実行不可
AllSigned
スクリプトにデジタル署名が必要 (無いと実行不可)
RemoteSined
ネットからDLしたスクリプトはデジタル署名が必要
Unrestricted
デジタル署名不要で実行可 (警告表示はある)
Bypass
一切の制限なしで全て実行可能 (警告も表示されない)
Bypass なら警告表示も出ないのでスクリプト実行が最も容易。(当然実行するスクリプトに安全上の問題がないことを確認してから実行。)

各実行ポリシーで.ps1スクリプトを呼び出し実行した時の挙動

引数で指定したポリシー
実行時の挙動
RemoteSigned
警告が表示され実行できない
Unrestricted
実行するか否か尋ねられる
Bypass
即実行される

※Windows本体のポリシー設定は(初期設定の)Restrictedのまま。

本体側のポリシー設定とは関係なくバッチ内でを指定したポリシー下で実行される

以下、各実行ポリシーの下でPowerShellのバージョン情報を表示するスクリプト($PSVersionTable)を実行した場合に表示される画面。

(実行したのはWebサイトからダウンロードした署名なしの.ps1ファイル)

RemoteSigned

実行ポリシーがRemoteSignedの時にPowerShellスクリプトを実行した際のエラー表示

ネットからダウンロードしたスクリプトを実行しようとした場合、デジタル署名されていませんという赤字の警告表示が出て実行できない。

Unrestricted

実行ポリシーがUnsignedの時にPowerShellスクリプトを実行した際の確認表示

ネットからダウンロードしたスクリプトの実行は可能だがセキュリティ警告が表示される。

実行する場合はRキーを押す必要があるため、スクリプト実行時に余計な手間がかかり不便。

実行ポリシーがUnsignedの時にPowerShellスクリプトを実行した際の確認表示に対して、Rキーを押した後

Rキーを押し「一度だけ実行する」を選べば、スクリプトの実行自体は可能。

Bypass

実行ポリシーがBypassの時にPowerShellスクリプトを実行すると特に警告も無くスクリプトが実行される

何の警告や中断も入らず実行される。(署名の有無に関係なく無警告で実行可能)

安全が分かっているスクリプトを実行する際には一番便利。

※SmartScreenをOFFにしている場合

  • SmartScreenをOFFにしている環境?では(結果的に)引数で指定したポリシー設定とは関係なくバッチが即座に実行される?
    (おそらく、ダウンロードしたバッチファイルにZoneIDが付与されていなかった)
  • 自宅PCだとスマートスクリーンをOFFにしている関係なのか(?)初回実行時の保護スクリーンが出ず、実行時の実行するか否かのお尋ねも無かった…。
    ↑Microsoft Defender SmartScreen をOFFにしているとそもそもダウンロードされたときにZoneIDが付与されず、ネットからダウンロードした署名無しスクリプトであってもRemoteSignedポリシー下で実行できてしまうのかな?
  • ※スクリプト本体をコピーしたかどうかはSmartScreenやプロンプト上での挙動に全く関係がない模様。(バッチ経由でコピーしようがZoneIDはコピー後のファイルにも付与されるため意味がない)
    ↑copyコマンドではなく、1バイトずつ読み込んで別のファイルに書き出すコマンドにすればZoneIDを伴わない複製も可能…?

 

その他仕様など雑多メモ (PowerShell)

PowerShell関数の命名規則

関数名には命名規則があり、予め承認されている動詞の一覧の中から動詞を選んで使う。

リンク:PowerShell関数の命名規則 & 承認されている動詞の一覧

使いたかった Create や Make といった動詞が無い…。

Out-Fileとかあるので、Out- を使った。

(Out-BatchesForDistribution:配布用バッチの出力)

ファイルパスから拡張子だけ取り出す

ファイルパスから拡張子だけを取り出したい時、split-path の -Extension オプションは PowerShell 6.0 からの導入なので、自分の環境では使用できなかったorz

→ 代わりに [System.IO.Path] を使用。

ファイル名・パス操作の便利な参照用資料

リンク:PowerShellメモ パス関連操作 - Qiita

角括弧 [ ] 入りファイル名への対応

角括弧 [ ] がファイル名に入っているとワイルドカード文字列の一部として認識されてしまい意図しない動作になりかねないので、コマンドでパスを指定する際は -Path パラメータではなく代わりに -LiteralPath パラメータを用いた。

-LiteralPath ならファイル名を文字通り受け取ってくれる。(ワイルドカードと解釈されない)

角括弧[]がファイル名に入ったスクリプトにも対応するため、path指定は必ず -LiteralPathパラメータを用いた。

参考情報

リンク:PowerShellのメモ~PathとLiteralPathパラメータについて - Qiita

Split-Path で -LiteralPath が実質使えない問題

しかし Split-Path は -LiteralPath を指定すると -Parent オプションが使えないので、結局 -Path を使わざるを得なかった。(つまり結局大かっこ問題には対応できていない)

リンク:Split-Path (Microsoft公式ドキュメント)

ファイルパスを扱う全ての標準コマンドが -LiteralPath に対応しているわけではなさそう…。

※ -Parent オプションを使わなければ -Path の代わりに -LiteralPath を使用可能。

(本当に個人的なメモ)

記事化前の最新情報はこちらで先にツイートしています。サイト更新告知もこちら。