Solo IT Hub

非エンジニアにバッチを配って一斉実行してもらうときの落とし穴 — ZIP・ダブルクリック・ログの行方

ZIPを解凍せず実行するとログが一時フォルダごと消える流れ メールのZIP添付を解凍せず、そのままダブルクリックして中のバッチを実行すると、Windowsが一時フォルダにバッチを展開して実行し、終了後にその一時フォルダごとログファイルを削除してしまう、という流れの図。対策は解凍してから実行する、またはログ出力先を固定フォルダにすること。 ZIPを解凍せず実行すると、ログが一時フォルダごと消える ① メール PW更新.zip 添付を解凍せず ② そのままダブルクリック Windowsが一時フォルダに 自動展開して実行 %Temp%\Temp1_... など ③ バッチが実行・成功 ログを %~dp0 に出力 = 一時フォルダの中 成功時に自身も自己削除 ④ 一時フォルダごと自動削除 ログも一緒に消えて回収不能 対策 ・必ず「すべて展開(解凍)」してから実行する ・ログ出力先を %~dp0 ではなく デスクトップ等の固定フォルダにする ・画面の「成功」表示を控えてもらう Outlookから直接開くと一層深い一時フォルダになる 技術的には正しく動いている。問題は「解凍する」という一手間が抜けたこと
// シリーズ:社内パスワード一斉更新プロジェクト(全4回)
  1. 第1回:バッチでパスワードを変更したらOutlookとTeamsの認証が消えた
  2. 第2回:非エンジニアにバッチを配って一斉実行してもらうときの落とし穴(この記事)
  3. 第3回:Windows・Microsoft 365・Google・NASで共通して使えるパスワードの文字
  4. 第4回:LanDiskのCSV一括登録で既存ユーザーのパスワードを更新する

新パスワードを埋め込んだバッチファイルを利用者ごとに作り、ダブルクリックで実行してもらう——。手順としては単純だが、配布先が数十人いて、しかもITに不慣れな人が混ざっていると、技術的には正しく動くはずのバッチが思わぬところで崩れる。
この記事は、その「現場で実際に起きた落とし穴」を時系列ではなくテーマ別にまとめたものだ。バッチ自体のパスワード変更ロジックについては第1回を参照してほしい。

前提:バッチはメールにそのまま添付できない

まず配布方法から。多くのメール環境は .bat の添付を自動でブロックする(受信側のメールソフトやウイルス対策ソフトが弾く)。共有フォルダに置く案は、他の利用者からも見えてしまいパスワードの観点で不採用にした。

結局、バッチをZIP圧縮してメール添付する形に落ち着いた。これで配送はできるが、まさにこのZIPが次の落とし穴を生む。

パスワードを埋め込んだバッチは「管理者に昇格→パスワード変更→自己削除」という、マルウェアに似た動きをする。ウイルス対策ソフトに実行をブロックされる可能性があるので、配布前にテスト機で必ず実行確認しておくこと。

落とし穴①:ZIPを解凍せず実行して、ログが消える

バッチには実行結果(成功/失敗)をテキストファイルに残すログ機能を付けていた。集計のためだ。ところが、ある利用者のログだけ回収できなかった。

その人がやったのは、Outlookで届いたZIP添付をダブルクリックで開き、開いたウィンドウの中のバッチを、解凍せずにそのままダブルクリックする、という操作だった。

ここで何が起きるか。ログの出力先はバッチ自身の置き場所(%~dp0)に設定していた。しかしZIPを解凍せずに実行すると、Windowsはバッチを一時フォルダに自動展開して実行する。つまりログは一時フォルダの中に書かれる。そしてバッチが終了すると、Windowsはその一時フォルダごと中身を削除する。ログも一緒に消える。

さらに、バッチは成功時に自分自身を del "%~f0" で削除する設計だったため、痕跡が二重に消えた。技術的にはすべて正しく動いていて、「解凍する」という一手間が抜けただけで、証跡が残らなかったのだ。

Outlookの添付から直接開いた場合、展開先は通常の %Temp% よりさらに奥のOutlook専用のセキュア一時フォルダになり、エクスプローラーからは一段と見つけにくい。回収を試みるなら %Temp% 配下の Temp1_ で始まるフォルダや、エクスプローラーで LOG_ を検索するのが手がかりになる。ただし削除済みなら戻らない。

対策

落とし穴②:ダブルクリックすると一瞬で閉じる(PowerShellのPATH問題)

テストでは右クリック →「管理者として実行」でずっと検証していて、何の問題もなかった。ところが本番を想定してダブルクリックで実行したところ、一瞬コマンドプロンプトが出てすぐ閉じる。ログも出ず、バッチも消えない。

非管理者のコマンドプロンプトから手動で実行すると、原因が見えた。

管理者権限で再実行します...
'powershell' は、内部コマンドまたは外部コマンド、
操作可能なプログラムまたはバッチ ファイルとして認識されていません。

バッチは「管理者権限がなければ、PowerShellの Start-Process -Verb RunAs で自分自身を昇格して再起動する」設計だった。そのPowerShellを短縮名(powershell)で呼んでいたため、環境変数 PATH にPowerShellのパスが含まれない環境では、昇格処理そのものが起動できずに即終了していた。

対策:PowerShellはフルパスで呼ぶ

UAC昇格でも、その後のPowerShell呼び出しでも、短縮名ではなくフルパスで指定すればPATHに依存しなくなる。

%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe -Command "Start-Process '%~f0' -Verb RunAs"

配布先のPCは環境がそろっていない。自分のテスト機で動いたからといって、全台で動くとは限らない。外部コマンドはフルパスで呼ぶのが、一斉配布バッチの基本姿勢になる。

「以前はダブルクリックすると昇格ダイアログが出たのに、今は出なくなった」——これはWindowsの仕様変更ではなく、このPATH問題であることが多い。OSのせいにする前に、バッチが昇格処理にたどり着けているかを疑う。

落とし穴③:SmartScreen/確認ダイアログが想定より増える

署名のないバッチや、ダウンロード・メール経由で届いたファイルには、WindowsがSmartScreen(スマートアプリコントロール)の警告を出すことがある。「WindowsによってPCが保護されました」といった青い画面だ。

利用者から見れば、想定していたUACの「このアプリがデバイスに変更を加えることを許可しますか?」だけでなく、別の確認ダイアログが追加で1〜2回出ることになる。手順書に「『はい』を押すダイアログは1回だけ」と書いていると、増えたダイアログで利用者が手を止めてしまう。

対策

落とし穴④:自己削除と「2回実行」の罠

バッチは全処理が成功したら自分を削除する設計にした。成功したファイルが残っていると、誰かが再実行して二重処理になるのを防ぐためだ。ここに2つの注意がある。

1ファイル1ユーザーだと、複数台で実行できない

1人が複数台のPCを使っている場合、ユーザー単位で1ファイルにすると、1台目で成功した瞬間にファイルが消え、2台目で実行できなくなる。ZIPから再展開すれば対処できるが、非技術者には酷だ。バッチはPC単位で作ることで、複数台でも順に実行できるようにした。

あわせて、「正しい人が、間違ったPCで実行する」事故も防ぎたい。バッチの先頭で対象PC名(%COMPUTERNAME%)が想定どおりかをチェックし、違えば中断する。次の数行をバッチの冒頭(管理者昇格の直後)に入れておけばよい。

REM ===== 対象PCのチェック =====
REM TARGET_PC にこのバッチを実行してほしいPC名を入れておく
set "TARGET_PC=PC-EIGYO-01"

if /i not "%COMPUTERNAME%"=="%TARGET_PC%" (
    echo エラー: このバッチは別のPC用です。
    echo   対象PC : %TARGET_PC%
    echo   このPC : %COMPUTERNAME%
    pause
    exit /b 1
)

/i は大文字小文字を区別しない比較。%COMPUTERNAME% はWindowsが常に持っている環境変数なので追加準備はいらない。ついでに、ログオン中のユーザーIDが想定どおりかも次の数行で確認できる(whoamiPC名\ユーザー名 を返すので、\ 区切りの2つ目を取り出す)。

REM ===== ログオンユーザーIDのチェック =====
set "TARGET_USER=user01"
for /f "tokens=2 delims=\" %%a in ('whoami') do set "CURRENT_USER=%%a"

if /i not "%CURRENT_USER%"=="%TARGET_USER%" (
    echo エラー: ログイン中のユーザーが対象と異なります。
    echo   対象ユーザー : %TARGET_USER%
    echo   現在のユーザー : %CURRENT_USER%
    pause
    exit /b 1
)

2回実行すると「失敗」表示で混乱する

第1回で触れた「変更」方式(NetUserChangePassword)は旧パスワードを必要とする。1回目の実行でパスワードが新しくなった後、同じバッチをもう一度実行すると、バッチに埋まっている旧パスワードはもう一致しないのでパスワード変更が「失敗」と表示される

これは破壊的ではない(すでに新パスワードになっている)が、画面に「失敗」と出れば利用者は不安になる。実際、「ログが消えた人」のPCも、調べると1回目で変更は成功しており、2回目以降が旧パスワード不一致で失敗していただけだった。新パスワードでリモート接続できたことが、1回目成功の動かぬ証拠になった。

運用上のもう一点。ZIPの中には平文のパスワードを含むバッチが残る。 バッチは成功時に自己削除するが、配ったZIPは利用者の手元に残る。全員の成功を確認したあとで、ZIPの削除を案内するとよい。

そもそも数十個のバッチとログを手作業で扱わない

ここまでの落とし穴は「実行してもらう側」の話だが、配る側・集める側にも限界がある。数十人分のバッチを手書きで作り、戻ってきた数十個のログを目視で確認するのは現実的ではない。ここは仕組みで殴る。

バッチはExcelから自動生成する

利用者ID・PC管理番号・新パスワードなどを一覧にしたExcel(マクロ付き)を用意し、1行=1台ぶんのバッチを自動生成するようにした。あわせて次のことも自動化すると、配布作業がぐっと楽になる。

サンプル:Excel VBAでバッチを自動生成する(形式チェック+ZIP圧縮込み)

「ユーザー一覧」シートに A:PC名 / B:ユーザーID / C:新パスワード / D:旧パスワード、「設定」シートの B1 に出力先フォルダを入れておく前提の例。実運用に合わせて列やパスを調整して使う。

' === バッチ一括生成(送付先フォルダ分け+ZIPまで) ===
Sub GenerateBatchFiles()
    Dim wsCfg As Worksheet, wsUser As Worksheet
    Set wsCfg = ThisWorkbook.Sheets("設定")
    Set wsUser = ThisWorkbook.Sheets("ユーザー一覧")

    Dim outRoot As String
    outRoot = wsCfg.Range("B1").Value                 ' 出力先フォルダ
    If Right(outRoot, 1) <> "\" Then outRoot = outRoot & "\"

    Dim r As Long, made As Long
    For r = 2 To wsUser.Cells(wsUser.Rows.Count, 1).End(xlUp).Row
        Dim pcName$, uid$, newPw$, oldPw$
        pcName = Trim(wsUser.Cells(r, 1).Value)
        uid    = Trim(wsUser.Cells(r, 2).Value)
        newPw  = Trim(wsUser.Cells(r, 3).Value)
        oldPw  = Trim(wsUser.Cells(r, 4).Value)
        If uid = "" Then GoTo NextRow

        ' --- 形式チェック:NGならバッチを作らずに印を付ける ---
        If Not IsValidPassword(newPw) Then
            wsUser.Cells(r, 5).Value = "NG(形式不正)"
            GoTo NextRow
        End If
        wsUser.Cells(r, 5).Value = "OK"

        ' --- 送付先フォルダ(ユーザーIDごと)を作成 ---
        Dim dstDir$
        dstDir = outRoot & uid & "\"
        If Dir(dstDir, vbDirectory) = "" Then MkDir dstDir

        ' --- バッチ本文を組み立て、Shift-JISで書き出し ---
        Dim batPath$, body$
        batPath = dstDir & "PW_" & pcName & "_" & uid & ".bat"
        body = MakeBatch(pcName, uid, newPw, oldPw)   ' ←バッチ本文を返す自作関数
        WriteShiftJis batPath, body                    ' 日本語を含むので Shift-JIS

        ' --- 同じ場所に ZIP も作る(メール添付用) ---
        MakeZip batPath, dstDir & "PW_" & pcName & "_" & uid & ".zip"
        made = made + 1
NextRow:
    Next r
    MsgBox made & " 件のバッチ(.bat+.zip)を生成しました。"
End Sub

' テキストを Shift-JIS で保存(バッチは日本語を含むため)
Sub WriteShiftJis(path$, body$)
    With CreateObject("ADODB.Stream")
        .Type = 2: .Charset = "Shift_JIS": .Open
        .WriteText body
        .SaveToFile path, 2          ' 2 = 上書き保存
        .Close
    End With
End Sub

' PowerShell の Compress-Archive で ZIP 化(同期実行で確実に作る)
Sub MakeZip(srcFile$, zipPath$)
    Dim ps$
    ps = "powershell -NoProfile -Command ""Compress-Archive -Path '" & srcFile & _
         "' -DestinationPath '" & zipPath & "' -Force"""
    CreateObject("WScript.Shell").Run ps, 0, True   ' 第3引数 True = 完了を待つ
End Sub

' パスワード形式チェック:15〜20文字/半角ASCIIのみ/!%" 禁止/4種中3種以上
Function IsValidPassword(pw$) As Boolean
    Dim i As Long, c As Long
    Dim hasU As Boolean, hasL As Boolean, hasD As Boolean, hasS As Boolean
    IsValidPassword = False
    If Len(pw) < 15 Or Len(pw) > 20 Then Exit Function
    For i = 1 To Len(pw)
        c = AscW(Mid(pw, i, 1))
        If c < 32 Or c > 126 Then Exit Function            ' 全角・全角スペース・制御文字を排除
        If InStr("!%" & Chr(34), Mid(pw, i, 1)) > 0 Then Exit Function  ' バッチ禁止文字 ! % "
        Select Case c
            Case 65 To 90:  hasU = True
            Case 97 To 122: hasL = True
            Case 48 To 57:  hasD = True
            Case Else:      hasS = True
        End Select
    Next i
    Dim kinds As Long
    kinds = Abs(hasU) + Abs(hasL) + Abs(hasD) + Abs(hasS)
    IsValidPassword = (kinds >= 3)
End Function

MakeBatch は、これまで紹介したUAC昇格・PC名チェック・パスワード変更などを組み立てて文字列で返す自作関数に置き換える。ポイントは、NGのパスワードはバッチを作らずスキップし、生成漏れを一覧(E列)で見えるようにしておくこと。

ログは統一フォーマットで出して、一括取り込み

バッチが吐くログをタブ区切りなどの統一フォーマットにしておけば、Excelマクロでフォルダ内のログをまとめて読み込み、PC単位・利用者単位で自動集計できる。

ログは五月雨式に届く。届くたびに差分だけ足そうとすると重複や取りこぼしが起きやすいので、取り込みのたびに「集計シート側」を一度クリアし、フォルダ内の全ログを毎回まるごと読み直す方式が運用は楽だった。

初期化するのは取り込み先のExcelシートであって、ログの入っているフォルダではない。フォルダの中身を消すと集めたログそのものが失われる。「シートをクリア → フォルダの全ログを再読み込み」の順を間違えないこと。

同じPC・同じ利用者のログが複数あるときは(=二重実行など)、ファイル名や中身の日時を比較して最新のものだけ採用すれば自然に整理できる。日時をゼロ埋めの YYYY/MM/DD HH:MM:SS 形式でログに出しておくと、文字列のまま大小比較で新旧を判定できる。

サンプル:Excel VBAでログを一括取り込み(シートを初期化+最新のみ採用)

「設定」シートの B2 にログ格納フォルダ、「ログ集計」シートに結果を出す前提。バッチは PC名 [Tab] ユーザーID [Tab] PW変更 [Tab] … [Tab] 実行日時 の形式でログを書く想定。

' === ログ一括取り込み(集計シートを初期化してから全件再読み込み) ===
Sub ImportLogs()
    Dim wsCfg As Worksheet, wsOut As Worksheet
    Set wsCfg = ThisWorkbook.Sheets("設定")
    Set wsOut = ThisWorkbook.Sheets("ログ集計")

    ' --- ★初期化するのは「シート」。フォルダには触れない ---
    wsOut.Cells.Clear
    wsOut.Range("A1:G1").Value = _
        Array("PC名", "ユーザーID", "PW変更", "メンテAcct", "PIN", "残留数", "実行日時")

    Dim folder$
    folder = wsCfg.Range("B2").Value
    If Right(folder, 1) <> "\" Then folder = folder & "\"

    ' 同一PC×IDは「最新の1行」だけ残す
    Dim latest As Object
    Set latest = CreateObject("Scripting.Dictionary")

    Dim f$
    f = Dir(folder & "LOG_*.txt")
    Do While Len(f) > 0
        Dim lines() As String, i As Long
        lines = Split(ReadShiftJis(folder & f), vbCrLf)
        For i = 0 To UBound(lines)
            If InStr(lines(i), vbTab) > 0 Then
                Dim c() As String
                c = Split(lines(i), vbTab)
                If UBound(c) >= 6 Then
                    Dim key$, dt$
                    key = c(0) & "|" & c(1)            ' PC名|ユーザーID
                    dt = c(6)                          ' 実行日時(ゼロ埋め)
                    ' 未登録、または日時が新しければ採用
                    If Not latest.Exists(key) Then
                        latest(key) = lines(i)
                    ElseIf dt > Split(latest(key), vbTab)(6) Then
                        latest(key) = lines(i)
                    End If
                End If
            End If
        Next i
        f = Dir
    Loop

    ' --- 採用した行をシートへ書き出し ---
    Dim r As Long, k As Variant
    r = 2
    For Each k In latest.Keys
        Dim cc() As String, j As Long
        cc = Split(latest(k), vbTab)
        For j = 0 To UBound(cc)
            wsOut.Cells(r, j + 1).Value = cc(j)
        Next j
        r = r + 1
    Next k
    MsgBox "ログ取り込み完了:" & latest.Count & " 件"
End Sub

' Shift-JISのテキストファイルを丸ごと読み込む
Function ReadShiftJis(path$) As String
    With CreateObject("ADODB.Stream")
        .Type = 2: .Charset = "Shift_JIS": .Open
        .LoadFromFile path
        ReadShiftJis = .ReadText
        .Close
    End With
End Function

毎回 wsOut.Cells.Clear集計シートだけを空にしてから読み直すので、二重実行ぶんも最新優先で勝手に整理される。フォルダのログは消さないので、後から検証もできる。

まとめ:詰まるのは技術ではなく「人の手順」

振り返ると、ここで挙げた落とし穴の大半はバッチのロジックの誤りではない。ZIPを解凍しない、フルパスを書いていない、想定外のダイアログ、二重実行——いずれも「正しく動くコードが、現場の操作で空振りする」種類の問題だ。

手作業でやっていた操作を肩代わりするツール(今回ならバッチファイル)を、ITに詳しくない人に渡して各自で実行してもらうときは、コードが正しいことと、現場で正しく実行されることは別物だと割り切る。ログは消えない場所へ、外部コマンドはフルパスで、手順書は「増えるダイアログ」を前提に、配布と集計は仕組みで。これだけで、一斉作業の取りこぼしは大きく減る。

// 次の記事
  1. 第3回:Windows・Microsoft 365・Google・NAS(LanDisk)で共通して使えるパスワードの文字 — バッチで使えない3記号も含めて