はじめに
会社では運用チームに属していて、各県にある30近い工場の運用作業を行っています。
システムの導入作業があると工場ごとに設定ファイルを変更して導入しており、現在は設定ファイルの編集は手作業で行っている。
導入作業の中で面倒なのが、あるClickOnceアプリケーションで、設定ファイル内でIPアドレスを変更する箇所が2ファイルある上に、ClickOnceのインストールのURLのIPアドレスも変更するので計3箇所あります。
IPアドレスを1回入力したら3箇所変更するように出来れば、ミスも無くなり作業も簡単になるわけです。
今回、一念発起してツールを作成しました。また、夏休み期間を利用して多くの人にも使えるようにブラッシュアップしてみました。
※ClickOnceのインストールのURLの変更は「mage.exe」を使用するので今回の編集ツールとは関係ないです。
【2024/10/04追記】
近頃、設定はJSONファイルが主流になってきています。
筆者はJSONの場合には下記ツールを使って置換しています。
【2019/10/21追記】
サーバーリプレース作業(Windows Server 2008R2 + Oracle11g -> Windows Server 2016 + PostgreSQL9.6)で使用アプリケーションの各設定ファイルの確認一覧用に「GETVALUE」を使用して値を取得するバッチを作成した際に気がついたことを追記しました。
【2020/08/26追記】
空要素(例 < DRIVE />)に対応しました。
設定変更を依頼された項目が空要素になっており、GETVALUEで次のタグを取得してしまったり、設定変更が正しく出来なかったので修正しました。
開発にあたり
xmlファイルの編集する上で最初に「MSXML2.DOMDocument」を使用したのですが、保存後に下記の問題が発生したので使用をやめました。
- 開始タグの末尾「 />」などの空白が消えてしまう。
- 空行が削除されてしまう
- 特殊文字がエスケープ文字に変換されてしまう
他にも問題があるかも知れません。やはり、指定した箇所以外の変更は何もして欲しくないわけです。
「MSXML2.DOMDocument」を使うことをやめたことで、ほぼ同じ方法でiniファイルの編集も出来るようになったので、これで良かったです。
2016/11/16追記
置換対象の値を取得できるといいなと思ったので、機能を追加しました。
使用方法
「ReplaceConfig.vbs」は引数を4つ指定できます。
ReplaceConfig FileName FindKey Value [CharSet]
-
FileName
設定ファイル(xml,ini)を指定します。 -
FindKey
変更するキーを指定します。
属性をキーにする場合は、「[]=角括弧」で囲んだ上で属性名の手前に「@」を付けてください。
例 configuration/appSettings/add[@key='url'] -
Value
変更する値を指定します。
空白を含む値は2重引用符で囲んでください。
特殊文字「"」や「%」などを値として使う場合は、「#(16進数)」としてください。2重引用符の「"」は「#22」となります。
属性の値を変更する場合は、「/@」を先頭に付けてください。
固定文字列「GETVALUE」にすると、置換ではなく置換対象の値を取得します。 -
CharSet
BOM無しの場合の文字コードをセットします。不要な場合は指定する必要はありません。指定しない場合、iniファイルの場合は「Shift-JIS」でそれ以外は「UTF-8」となります。xmlファイルの場合は「encoding」を優先します。
※設定ファイルを対象としているので、複数同一キー見つかった場合でも最初のみ変更して終了する仕様としています。
使用例
xmlファイルの変更
<?xml version="1.0" encoding="utf-8"?>
<system-config>
<system-name>ConfigSetValue</system-name>
<version>1.1</version>
<users>
<user id="0001" expired="2015/12/31">
<name>foo</name>
</user>
<user id="0002" expired="2015/09/30">
<name>bar</name>
</user>
</users>
</system-config>
バージョン番号を1.1から1.2に変更する。
ReplaceConfig system-config.xml system-config/version 1.2
ユーザー Id=0002の名前をbarからhogeに変更する。
ReplaceConfig system-config.xml system-config/users/user[@id='0002']/name hoge
ユーザー Id=0001の日付を2015/12/31から2016/08/17に変更する。
ReplaceConfig system-config.xml system-config/users/user[@id='0001']/@expired 2016/08/17
値の取得
バージョン番号の1.1を取得する。固定文字列「GETVALUE」を指定している。
for /f "usebackq tokens=*" %%i IN (`call cscript //nologo ReplaceConfig.vbs system-config.xml system-config/version GETVALUE`) DO @set result=%%i
echo %result%
【2019/10/21追記】
バッチで属性のキーを指定する際の「=」にはエスケープ処理が必要で、エスケープ文字の「^」を使用します。(例 id^='0001')
for /f "usebackq tokens=*" %%i IN (`call CScript //nologo ReplaceConfig.vbs system-config.xml system-config/users/user[@id^='0001']/@expired GETVALUE`) DO @set result=%%i
echo %result%
バッチでIF文を使用して括弧で囲んだ場合、遅延環境変数として「%」でなく「!」で囲まない(例 echo !result!)と値がセットされません。
setlocal enabledelayedexpansion
if exist system-config.xml (
for /f "usebackq tokens=*" %%i IN (`call CScript //nologo ReplaceConfig.vbs system-config.xml system-config/users/user[@id^='0001']/@expired GETVALUE`) DO @set result=%%i
echo !result!
)
endlocal
iniファイルの変更
[database]
host = 192.168.0.1
USER = username
PASS = password
[encoding]
enc = SJIS
hostを192.168.0.1からlocalhostに変更する。
ReplaceConfig db.ini database/host 127.0.0.1
encをSJISからUTF-8に変更する。
ReplaceConfig db.ini encoding/enc UTF-8
【2023/08/03追記】
仕事で使用してもらっていたところ、違うセクションの同じキーの値が更新されるとの報告がありました。
調査したところ、先頭の方にあったキーの値にセクション名が使われていたため、最初に見つかった違うセクションの同じキーの値を更新していました。
対応としてセクション名とキー名の検索に正規表現を使用しているため、セクション名部分を"\[" と "\]"で括ってもらうことで解決しました。
ReplaceConfig db.ini \[database\]/host 127.0.0.1
値の取得
hostの192.168.0.1を取得する。固定文字列「GETVALUE」を指定している。
for /f "usebackq tokens=*" %%i IN (`call cscript //nologo ReplaceConfig.vbs db.ini database/host GETVALUE`) DO @set result=%%i
echo %result%
【2019/10/21追記】
バッチでIF文を使用して括弧で囲んだ場合、遅延環境変数として「%」でなく「!」で囲まない(例 echo !result!)と値がセットされません。
setlocal enabledelayedexpansion
if exist db.ini (
for /f "usebackq tokens=*" %%i IN (`call cscript //nologo ReplaceConfig.vbs db.ini database/host GETVALUE`) DO @set result=%%i
echo !result!
)
endlocal
ソースリスト
'--------------------------------------
' 引数1: 変更するファイル
' 引数2: 検索文字列
' 引数3: 変更文字列
' 引数4: 文字コード(BOM以外)
'--------------------------------------
Option Explicit
'On Error Resume Next
Const ForReading = 1, ForWriting = 2, ForAppEnding = 8
Const adTypeBinary = 1, adTypeText = 2
Const adReadAll = -1, adReadLine = -2
Const typeXML = 1, typeINI = 2
Const GET_CONFIG = "GETVALUE"
Dim objArgs
Dim pathName, keyName, valueString, charSet, strLine
'コマンドライン引数取得
Set objArgs = WScript.Arguments
'対象パス名取得
If objArgs.Count >= 1 Then
pathName = objArgs(0)
Else
WScript.Echo "対象パス名が見つかりません"
WScript.Quit(1)
End If
'検索キー取得
If objArgs.Count >= 2 Then
keyName = objArgs(1)
Else
WScript.Echo "検索キーが見つかりません"
WScript.Quit(2)
End If
'変更対象値取得
If objArgs.count >= 3 then
valueString = objArgs(2)
Else
WScript.Echo "変更対象値が見つかりません"
WScript.Quit(3)
End If
'文字コード(BOM以外)
If objArgs.count >= 4 then
charSet = UCase(objArgs(3))
End If
'ファイル情報を取得する
Dim param, key, str, result
Set param = CreateObject("Scripting.Dictionary")
Call GetInfo(pathName, charSet)
'設定値変更処理
result = Main(pathName, keyName, valueString)
Set param = Nothing
WScript.Quit(result)
'------------------------------------------------------------------------
' 設定値変更処理
' pathName: 対象パス名
' keyName: 検索キー
' valueString: 変更対象値
'------------------------------------------------------------------------
Function Main(pathName, keyName, valueString)
Dim dic, reg, matches, match, hexvalue, stream_r, stream_pre, stream_w
Dim bin, newtxt, line, nextline, aryKey, i, idx, curKey, count, key
Dim attrKey, aryAttr, node, exitFlag, lfType, lfType2, allLine, aryLine
Dim result
Main = 0
Set stream_r = CreateObject("ADODB.Stream")
Set stream_pre = CreateObject("ADODB.Stream")
Set stream_w = CreateObject("ADODB.Stream")
Set dic = CreateObject("Scripting.Dictionary")
Set reg = CreateObject("VBScript.RegExp")
reg.Global = True
reg.IgnoreCase = True
'検索キー分割し、キー単位で状態を保持する
aryKey = Split(keyName, "/")
For i=0 To UBound(aryKey)
dic.Add aryKey(i), 0
Next
'現行の文字コードで読み込む
stream_r.Open
stream_r.CharSet = param("CharSet")
stream_r.LoadFromFile pathName
allLine = stream_r.ReadText(adReadAll)
stream_r.Close
'改行コード
lfType = ""
Select Case param("LfType")
Case "CrLf": lfType = vbCrLf
Case "Lf": lfType = vbLf
Case "Cr": lfType = vbCr
End Select
'XMLで改行無しの場合
lfType2 = lfType
If param("FileType") = typeXML And param("LfType") = "" Then
'一時的に改行を付ける
reg.Pattern = "(.*?>)(<.*?)"
allLine = reg.Replace(allLine, "$1" & vbCrLf & "$2")
lfType2 = vbCrLf
End If
'改行コードで分割
aryLine = Split(allLine, lfType2)
'設定値の#記号の数値変換
reg.Pattern = "#(\d\d)"
Set matches = reg.Execute(valueString)
For Each match In matches
hexvalue = match.SubMatches(0)
valueString = Replace(valueString, "#" & hexvalue, Chr(CInt("&H" & hexvalue)))
Next
Set matches = Nothing
i = 0
count = 0
exitFlag = False
For idx = 0 To UBound(aryLine)
line = aryLine(idx)
IF exitFlag = False Then
key = dic.Keys
node = key(i)
'キーの一致確認
If param("FileType") = typeXML Then
If Instr(node, "[") > 0 Then
node = Replace(node,"[@",".*?")
node = Replace(node,"='","=.*?")
node = Replace(node,"']","")
node = Replace(node,"]","")
End If
reg.Pattern = "<\s*?" & node & ".*?>"
Else
reg.Pattern = "^[^;]*?" & node & ".*?"
End If
If reg.Test(line) Then
'キー状態有りに設定
dic.Item(key(i)) = 1
'次のキーへ
i = i + 1
If i <= UBound(key) Then
'属性値の変更かチェック(先頭に"@"記号)
attrKey = ""
If Instr(key(i), "@") = 1 Then
attrKey = Mid(key(i), 2, Len(key(i)))
reg.Pattern = ".*?" & attrKey & ".*?="
If reg.Test(line) Then
'キー状態有りに設定
dic.Item(key(i)) = 1
End If
End If
End If
'キー有無状態の確認
count = 0
For Each curKey In dic
count = count + dic.Item(curKey)
Next
End If
'最終キーに到達
If count = UBound(aryKey) + 1 Then
'設定値の変更
If param("FileType") = typeXML Then
'XMLファイル側の変更
If attrKey <> "" Then
'属性値の変更
reg.Pattern = attrKey & "(.*?=.*?['""])(.*?)(['""])"
If reg.Test(line) Then
result = GetValue(reg, valueString, line)
line = reg.Replace(line, attrKey & "$1" & valueString & "$3")
End If
Else
'値の変更
reg.Pattern = "(>)(.*?)(<)"
If reg.Test(line) Then
result = GetValue(reg, valueString, line)
line = reg.Replace(line, "$1" & valueString & "$3")
Else
'終了タグ
reg.Pattern = "(?:<)([^\s]+)(.*)(?:\/>)"
If reg.Test(line) Then
result = ""
If valueString <> "" Then
line = reg.Replace(line, "<$1$2>" & valueString & "</" & "$1" & ">")
line = Replace(line, " >", ">")
End If
Else
'次の行に値がある
nextline = aryLine(idx + 1)
reg.Pattern = "(.*?)(\S.*)"
If reg.Test(nextline) Then
result = GetValue(reg, valueString, nextline)
'読み込んだ次の行を再セット
aryLine(idx + 1) = reg.Replace(nextline, "$1" & valueString)
End If
End If
End If
End If
Else
'Iniファイル側の値変更
reg.Pattern = "(=\s*)(.*)"
If reg.Test(line) Then
result = GetValue(reg, valueString, line)
line = reg.Replace(line, "$1" & valueString)
End If
End If
dic.Item(aryKey(count-1)) = 0
'変更対象値に変更したら処理終了
exitFlag = True
End If
End If
'読み込んだ行を再セット
aryLine(idx) = line
Next
If exitFlag = True Then
If UCase(valueString) = GET_CONFIG Then
'値を返す
WScript.Echo result
Else
'改行を付加
newtxt = Join(aryLine, lfType)
'ファイル保存
If param("CharSet") = "UTF-8" And param("IsBOM") = False Then
'BOM無し
stream_pre.Type = adTypeText
stream_pre.CharSet = param("CharSet")
stream_pre.Open
stream_pre.WriteText newtxt
stream_pre.Position = 0
stream_pre.Type = adTypeBinary
stream_pre.Position = 3
bin = stream_pre.Read()
stream_pre.Close()
stream_w.Type = adTypeBinary
stream_w.Open()
stream_w.Write(bin)
stream_w.SaveToFile pathName, ForWriting
stream_w.Close()
Else
stream_w.Open()
stream_w.Charset = param("CharSet")
stream_w.WriteText newtxt
stream_w.SaveToFile pathName, ForWriting
stream_w.Close()
End If
End If
Else
WScript.Echo "キーが見つかりませんでした。"
Main = 9
End If
Set dic = Nothing
Set reg = Nothing
End Function
'------------------------------------------------------------------------
'ファイルの文字コード等の情報を取得する
'------------------------------------------------------------------------
Function GetInfo(pathName, charSet)
Dim stream, reg, match, matches, bytCode, allLine, line, endLine, lfType, aryLine
Dim fso, ext
GetInfo = False
'初期値セット
param.Add "CharSet", "UTF-8" '文字コード
param.Add "IsBOM", False 'BOM有無
param.Add "FileType", typeINI 'ファイルタイプ
param.Add "LfType", "CrLf" '改行コード
param.Add "IsEndLf", False '末尾改行有無
'拡張子を取得する
Set fso = CreateObject("Scripting.FileSystemObject")
ext = fso.GetExtensionName(pathName)
Set fso = Nothing
'ファイルをオープン
Set stream = CreateObject("ADODB.Stream")
'BOMチェック用に先頭行を読み込む
stream.Type = adTypeBinary
stream.Open
stream.LoadFromFile pathName
bytCode = stream.Read
stream.Close
Call CheckBOM(bytCode)
If param("IsBOM") = False Then
'IniファイルはSJISとする
If charSet = "" And (LCase(ext) = "ini" Or Instr(LCase(pathName),".ini") > 1) Then
charSet = "Shift-JIS"
End If
param("CharSet") = charSet
End If
'文字コードを自動判定で読み込み
stream.Type = adTypeText
stream.CharSet = "_autodetect_all"
If charSet <> "" Then stream.CharSet = charSet
stream.Open
stream.LoadFromFile pathName
allLine = stream.ReadText(adReadAll)
stream.Close
'改行コード判定(100文字で判断)
line = Left(allLine, 100)
If Instr(line, vbCrLf) <> 0 Then
lfType = "CrLf"
ElseIf Instr(line, vbLf) <> 0 Then
lfType = "Lf"
ElseIf Instr(line, vbCr) <> 0 Then
lfType = "Cr"
End If
param("LfType") = lfType
'改行コードで分割
Select Case lfType
Case "CrLf": lfType = vbCrLf
Case "Lf": lfType = vbLf
Case "Cr": lfType = vbCr
End Select
aryLine = Split(allLine, lfType)
'先頭行に"< >"があれば、XMLとして扱う
Set reg = CreateObject("VBScript.RegExp")
reg.Pattern = "<.*?>"
If reg.Test(aryLine(0)) Then
param("FileType") = typeXML
If param("IsBOM") = False Then
'encodingの値を文字コードにセット
reg.Pattern = "encoding.*?=['""](.*?)['""]"
Set matches = reg.Execute(aryLine(0))
For Each match In matches
param("CharSet") = UCase(match.SubMatches(0))
Next
Set matches = Nothing
End If
End If
Set reg = Nothing
'最終行の改行コード有無
If aryLine(UBound(aryLine)) = "" Then
param("IsEndLf") = True
End If
GetInfo = True
End Function
'------------------------------------------------------------------------
'BOMチェックの情報を取得する
'------------------------------------------------------------------------
Function CheckBOM(str)
Dim bytes(2), result
bytes(0) = AscB(MidB(str, 1, 1))
bytes(1) = AscB(MidB(str, 2, 1))
bytes(2) = AscB(MidB(str, 3, 1))
If LenB(str) > 1 Then
If bytes(0) >= &HFE And bytes(1) >= &HFE Then
result = "unicode"
If bytes(0) = &HFF And bytes(1) = &HFE Then
result = "unicodeFFFE"
End If
Else If bytes(0) = &HEF And bytes(1) = &HBB And bytes(2) = &HBF Then
result = "UTF-8"
End If
End If
End If
If result <> "" Then
param("CharSet") = result
param("IsBOM") = True
End If
CheckBOM = result
End Function
'------------------------------------------------------------------------
'現在の設定値を取得する
'------------------------------------------------------------------------
Function GetValue(reg, valueString, target)
Dim matches, match
GetValue = ""
If UCase(valueString) = GET_CONFIG Then
Set matches = reg.Execute(target)
For Each match In matches
GetValue = match.SubMatches(1)
Exit For
Next
End If
End Function
ライセンスっぽいこと
- コード改変や配布は自由です。
- このツールによる義務/責任を何ら負いません。
最後に
仕組みとしては、変更キーを"/"区切りで分割して連想配列のキーとし値を「0」でセット、キーが見つかれば「1」に変更して、全てのキーの値が「1」になれば値を変更するようにしています。
今回ブラッシュアップした内容としては、下記となります。
- 特殊文字「"」や「%」などの対応
- 文字コード関連
- 改行無しXMLファイルの対応
- キーと値の間の空白やタグ間の改行対応
設定ファイルも手作業で修正してもそんな手間ではないわけですが、こういうツールがあれば、バッチを使って値を変更できるようになるので作業も簡単になりミスもなくなります。