Programming Field

JSONに記述されたデータを環境変数にセットするバッチファイル

JSONデータを解析し、指定したパターンに一致するデータがあれば、そのプロパティー名(「hoge.piyo」など)を名前とした環境変数にデータを設定するバッチファイルです。

ソースコード

[SetEnvFromJson.bat]

@echo off & goto DoExec
<#
----------------------------------------
Usage:
  SetEnvFromJson -Pattern "name-pattern" -Data "json-data" [-Prefix "env-prefix"]
  SetEnvFromJson -Pattern "name-pattern" [-Prefix "env-prefix"] < json-file-data
  any-json-output-command | SetEnvFromJson -Pattern "name-pattern" [-Prefix "env-prefix"]
----------------------------------------
### Batch file
:DoExec
rem -- 拡張構文が利用可能かどうかの確認
rem -- (「%~dp0」構文が正しく展開されず「~dp0」となったり Set コマンドに失敗したりしたら使用不可)
setlocal
set TEMP_VAR=
set "TEMP_VAR=%~dp0" 2>NUL
if errorlevel 1 goto NoExtensions
if "%TEMP_VAR%"=="~dp0" goto NoExtensions
if "%TEMP_VAR%"=="" goto NoExtensions
endlocal

rem -- 「SetEnvFromJson.bat」があるディレクトリに「'」文字が含まれても良いように事前に pushd する
pushd "%~dp0"
rem -- 「PARAMETERS」変数を使うため一時的に Setlocal する
setlocal
rem -- バッチファイルの引数全てを「PARAMETERS」に展開
set PARAMETERS=%*
rem -- 「,」がスペース文字に変換されないように「^,」に置換
set "PARAMETERS=%PARAMETERS:,=^,%"
rem -- 「"」がPowerShellによって文字列の括りと解釈されるように「\"」に置換
rem -- (置換しないとプログラム引数としての「" "」と扱われ除去されてしまう)
set "PARAMETERS=%PARAMETERS:"=\"%"
rem -- Endlocal で「PARAMETERS」変数をリセットしつつ、その値を PowerShell の引数に指定し、
rem -- PowerShell の実行結果(出力)を For に渡す
rem -- (「2^>CON」が必要な理由は後述)
endlocal & for /f "tokens=1,* delims==" %%A in (' ^
  PowerShell.exe -Command "& (iex -Command ('{#' + ((gc '%~nx0') -join \"`n\") + '}'))" %PARAMETERS% 2^>CON ^
  ') do (
    rem -- 出力は行ごとに「name=value」となっており A に name, B に value が
    rem -- 入るため、Set の左側に A の値、右側に B の値を指定する
    set "%%A=%%B"
)
popd
exit /b 0

rem -- 拡張構文が利用できない場合、0 以外の終了コードを返す
rem -- Setlocal で改めて拡張構文を有効にしつつ終了させるが、それもできない場合は Goto で末尾に飛ばす
rem -- (拡張構文が利用できない場合「goto :EOF」も不可)
:NoExtensions
echo You must enable extensions first.
setlocal enableextensions
exit /b 1
goto Finish
----------------------------------------
### PowerShell script
#>
# 「Prefix」と「Data」は省略可能とし、「Data」を省略した場合は標準入力からデータを受け取る
Param(
    [parameter(Mandatory=$true)][string] $Pattern,
    [string] $Prefix,
    [string] $Data
)
$baseJson = ""
if ($Data -eq $null)
{
    $baseJson = [System.Console]::In.ReadToEnd()
}
else
{
    $baseJson = $Data -join ' '
}
if ($Prefix -eq $null)
{
    $Prefix = ""
}

# 文字列をJSONデータとしてオブジェクトに変換
$obj = $baseJson | ConvertFrom-Json

# 得たオブジェクトからパターンに一致するメンバを取り出す
# ($Pattern が "A.B.*" なら $obj.A.B 以下すべてを得る)
$nextTargetObj = $obj
$targetMembers = $null
$prefixName = ""
$nextPrefixName = $Prefix
($Pattern -split "\.") | ForEach-Object {
    $obj = $nextTargetObj
    $prefixName = $nextPrefixName
    if ($obj -ne $null)
    {
        $a = $nextTargetObj | Get-Member $_ -MemberType NoteProperty
        if ($a.Length -eq 0)
        {
            $targetMembers = $null
            $nextTargetObj = $null
        }
        else
        {
            $targetMembers = $a
            $nextTargetObj = $obj.($targetMembers[0].Name)
            $nextPrefixName = $_ + "."
        }
    }
}

# メンバが得られたら、「名前=値」という形式でそれらを出力する
# (呼び出し元バッチファイルの For 文でそれを解析する)
if ($targetMembers -ne $null)
{
    $targetMembers | ForEach-Object {
        Write-Output ($prefixName + $_.Name + "=" + $obj.($_.Name))
    }
}
# 「:Finish」と記述してもエラーにならないように「:Finish」という関数を用意
function :Finish { }
# これにより、以下の1行がバッチとしてもPowerShellとしても有効になる
:Finish

使い方

例1

(コマンド実行例)

SetEnvFromJson.bat -Pattern "*" -Data '{ Data1: "Hoge", Data2: "Piyo"}'
echo Data1=%Data1%, Data2=%Data2%

例2

(ファイル例: Settings.json)

{
    MyProgram: {
        Disabled: true,
        DefaultName: "Rola"
    }
}

(実行するバッチファイル)

@echo off
setlocal enableextensions
call SetEnvFromJson -Pattern "MyProgram.*" -Prefix MyEnv. < Settings.json
if not "%MyEnv.MyProgram.Disabled%"=="" goto Finish
MyProgram output.log
:Finish

解説など

  • 大まかな流れは「バッチファイルで引数を準備」→「PowerShellでJSONを解析」→「解析結果をバッチファイルに戻して環境変数に設定」です。
  • このバッチファイルは「バッチファイルにPowerShellスクリプトを埋め込む その2」の方法を応用しています。
    • 拡張構文が無効である場合「exit /b」ができないため、ファイル末尾にGotoできるようにPowerShell側も一工夫しています。
  • PowerShell内で環境変数を設定してしまうと、それらの内容はPowerShell内でしか利用できないため、バッチファイル呼び出し元でも環境変数を伝播できるように結果をバッチファイルに戻す必要があります。
    • バッチファイルに戻すことにより、Callで本バッチファイルを呼び出すと呼び出し元にも環境変数が反映されます。
    • また、バッチファイル側でもバッチファイル呼び出し元で環境変数を利用できるようにするためにSetlocalが有効な状態で環境変数を設定しないようにしています。
    • PowerShell内から(その環境変数を用いて)本来の目的のプログラムを実行するのであれば、この手順は不要です。
  • PowerShellは、標準出力の出力先がリダイレクトされており(System.Console.IsOutputRedirected が True)、標準エラー出力の出力先がリダイレクトされていない(System.Console.IsErrorRedirected が False)場合、PowerShell内のエラーの出力先が標準出力となるようで、標準エラー出力を明示的にリダイレクトしておく必要があります。
    • Forは出力をキャプチャーするために標準出力をリダイレクトしています。