【BAT】アプリケーションのアンインストールを行う

# はじめに
お仕事でアプリケーションの旧バージョンからのアップグレード処理をバッチファイルで作成しました。
今回、その中のアンインストールする処理部分を一般公開用に作り直しました。

# アンインストール情報の取得
アンインストール情報はレジストリのアンインストールから取得していますので、[コントロール パネル]-[プログラム]-[プログラムと機能]にある一覧のアプリケーションが対象となります。

64bitと32bitのプラットフォームごとに取得するレジストリキーを変更しています。

```bash
if "%PROCESSOR_ARCHITECTURE%" EQU "AMD64" call :ARCH_X64 "%installTarget%" "%varsionTarget%"
if "%PROCESSOR_ARCHITECTURE%" EQU "x86" call :ARCH_X86 "%installTarget%" "%varsionTarget%"
```

## 64bit
|レジストリのアンインストール情報|
|:--|
|HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Uninstall\|
|HKEY_LOCAL_MACHINE\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\|
|HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Uninstall\|

## 32bit
|レジストリのアンインストール情報|
|:--|
|HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Uninstall\|
|HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Uninstall\|


# 仕様
管理者権限で実行するようにしています。

|引数|内容|
|:--|:--|
|第1引数|アプリケーション名(空白がある場合はダブルクォーテーションで囲む)|
|第2引数|バージョン（指定がない場合、バージョンチェックはしない)|

※アプリケーション名とバージョンの文字列検索は完全一致となっています。

```bash:例
> uninstall.bat "Microsoft Edge" 84.0.522.63

> uninstall.bat "Python 3.7.8 Executables (64-bit)"
```

アンインストールは、対象アプリケーションのレジストリ情報にある「UninstallString」を取得して実行しています。
文字列検索でFindを使用していることで「QuietUninstallString」の方を優先して取得してしまいます。
その為、「/SILENT」または「/S」のオプションを除外するようにしています。

%TEMP%\soft.txt ファイルにパイプ(|)区切りで、アプリケーション名、バージョン、アンインストール情報を出力しています。

# ソースコード
```:uninstall.bat
@echo off
openfiles > nul 2>&1 
if %ERRORLEVEL% equ 0 goto Start
echo 管理者権限で実行してください。
pause
exit

cd /d %~dp0

:Start
setlocal enabledelayedexpansion
set installTarget=%~1
set versionTarget=%~2

rem アンインストール情報で確認
del /q %TEMP%\soft.txt >nul 2>&1
if "%PROCESSOR_ARCHITECTURE%" EQU "AMD64" call :ARCH_X64 "%installTarget%" "%versionTarget%"
if "%PROCESSOR_ARCHITECTURE%" EQU "x86" call :ARCH_X86 "%installTarget%" "%versionTarget%"

find "%installTarget%" %TEMP%\soft.txt >nul 2>&1
if not ERRORLEVEL 1 (
  echo インストール済み
  for /f "delims=| tokens=1,2*" %%a in ('find "%installTarget%" %TEMP%\soft.txt') do (
    set version=%%b
    set uninstall=%%c
  )
  echo !version!
  echo !uninstall!

  rem アンインストール処理
  echo !uninstall! | find "MsiExec.exe" >nul 2>&1
  if not ERRORLEVEL 1 (
    set msiStr=!uninstall:"=!
    set msiStr=!msiStr:/I=/X!
    !msiStr! /norestart
  ) else (
    set exeStr=!uninstall:/SILENT=!
    set exeStr=!exeStr:/S=!

    call !exeStr!
  )
) else (
  echo 未インストール
)

endlocal

:END
echo.
echo 終了します。何かキーを押してください。
echo.
pause >nul
exit /b

rem ---------------------------------------------------------
rem アンインストール情報の取得
rem ---------------------------------------------------------
:ARCH_X64
rem x64環境
call :GET_UNINSTALL %1 %2 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\
if ERRORLEVEL 1 exit /b
call :GET_UNINSTALL %1 %2 HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\
if ERRORLEVEL 1 exit /b
call :GET_UNINSTALL %1 %2 HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Uninstall\

exit /b

:ARCH_X86
rem x86環境
call :GET_UNINSTALL %1 %2 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\
if ERRORLEVEL 1 exit /b
call :GET_UNINSTALL %1 %2 HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Uninstall\
if ERRORLEVEL 1 exit /b

exit /b

rem ---------------------------------------------------------
rem レジストリからアンイストール検索
rem ---------------------------------------------------------
:GET_UNINSTALL
for /f "tokens=*" %%a in ('reg query %3') do (
  set str=""
  for /f "tokens=2,*" %%I in ('reg query "%%a" /s 2^>nul ^| find "DisplayName"') do (
    set str=%%J
    if "!str!"==%1 (
      for /f "tokens=2,*" %%I in ('reg query "%%a" /s 2^>nul ^| find "DisplayVersion"') do (
        set ver=%2
        if "!ver!"=="""" set ver="%%J"
        if !ver!=="%%J" (
          set str=!str!^|%%J
          for /f "tokens=2,*" %%I in ('reg query "%%a" /s 2^>nul ^| find "UninstallString"') do (
            echo !str!^|%%J>>%TEMP%\soft.txt
          )
          exit /b 1
        )
      )
    )
  )
)

exit /b
```

# 実施テスト

## パターン１
UninstallStringが「MsiExec.exe /I」となっている場合、「MsiExec.exe /X /norestart」に変更して実行している。
サイレントアンインストールにしたい場合、プログラムを変更して「/quiet」オプションを追加すればいいです。
[msiexec インストールオプション](https://docs.microsoft.com/ja-jp/windows-server/administration/windows-commands/msiexec)

```bash:例
> uninstall.bat "Python 3.7.8 Executables (64-bit)"
```

![uninstall_pattern1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14053/2d5012f9-75ff-a835-a431-36525d5c54eb.png)

## パターン2
UninstallStringが「setup.exe --uninstall」となっている。

```bash:例
> uninstall.bat "Microsoft Edge" 84.0.522.63
```
![uninstall_pattern2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14053/ec754cb2-169f-3b9d-17d2-c26697caad55.png)

## パターン3
UninstallStringが「unins000.exe」（専用アンインストール）となっている。
今回は実行時にサイレントオプション「/SILENT」を除外している。

```bash:例
> uninstall.bat "WinMerge 2.16.6.11 x64"
```

![uninstall_pattern3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14053/6edad82a-8bfd-96f2-abb7-fd4663dc2e4d.png)

## パターン4
UninstallStringが「Uninstall」（専用アンインストール）となっている。
今回は実行時にサイレントオプション「/S」を除外している。

このパターンだけ、callコマンドを付けているが次の処理に流れてしまう。

```bash:例
> uninstall.bat "Unity Hub 2.3.2"
```

![uninstall_pattern4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14053/c91d6369-2f9d-5695-e61c-574490df1b3e.png)

## パターン5
UninstallStringが「rundll32.exe」（ClickOnceアンインストール）となっている。

```bash:例
> uninstall.bat ClickOnceTest 3.1.0.0
```
![uninstall_pattern5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14053/19a1f991-f70a-ecb0-5b2f-6036d04d8691.png)

# 最後に
今回はあえて、バッチファイルのみで書いてみました。
バッチファイルだと他言語では簡単なことを実現するのに分からなくてハマりますね。

```:２重引用符の削除
set installTarget=%~1
set msiStr=!uninstall:"=!
```

```:コマンドの実行結果を出力しない
rem >nul 2>&1 を付ける
find "%installTarget%" %TEMP%\soft.txt >nul 2>&1
```

```:遅延環境変数
rem 変数の中身を見るときに「%（パーセント）」ではなく「!（エクスクラメーションマーク）」で囲む
setlocal enabledelayedexpansion

  for /f "delims=| tokens=1,2*" %%a in ('find "%installTarget%" %TEMP%\soft.txt') do (
    set version=%%b
    set uninstall=%%c
  )
  echo !version!
  echo !uninstall!

endlocal
```

```:レジストリ情報のサブキーの値取得
rem 入れ子にしてサブキーの値取得
for /f "tokens=*" %%a in ('reg query %3') do (
  set str=""
  for /f "tokens=2,*" %%I in ('reg query "%%a" /s 2^>nul ^| find "DisplayName"') do (
```


あと、以前の記事「[【BAT】Microsoft Visual C++ 再頒布可能パッケージのインストール確認](https://qiita.com/yaju/items/5385908e81beb149e5d8)」でも書いたが、バッチのreg queryコマンドは遅いです。どうしても気になるなら、WSHやPowerShellにする方がいいでしょう。