【WSH】文字列置換(正規表現対応)ツール

# はじめに
会社では運用チームに属していて、各県にある30近い工場の運用作業を行っています。
システムの導入作業があると工場ごとに設定ファイルを変更して導入しており、現在は設定ファイルの編集は手作業で行っている。
その為、「[【WSH】設定ファイル(xml,ini)の編集ツール](http://qiita.com/yaju/items/45799934a516ff4f3504)」(ReplaceConfig.vbs)を作成することで効率化することが出来た。しかし、製品名を変更したいという要望もあり、もう一工夫必要になった。
ClickOnceの発行物を「MageUI.exe」を使うことで、製品名やバージョンやStart Locationなどの設定を変更することが出来ますが、製品名を変更しても「publish.htm」ファイルまでは更新されません。
製品名が書かれている「.application」ファイルの中身はXMLファイルであるため、編集ツール(ReplaceConfig.vbs)を使うことで変更できたが、「publish.htm」ファイルはHTMLであり特定できるキーが無いため、別途、正規表現で置換するツールを作成することにしました。
通常使われる文字コードや改行コードには対応しております。

2024/10/04追記
コメント付きのjsonファイルを置換したら、日本語コメントが文字化けする不具合を修正

2016/11/16追記
置換対象の値を取得できるといいなと思ったので、機能を追加しました。

# 使用方法
「ReplaceRegex.vbs」は引数を４つ指定できます。

```
ReplaceRegex.vbs FileName Search Value [CharSet]
```

* FileName
置換対象ファイルを指定します。

* Search
検索文字列(正規表現対応)を指定します。例 /ab+c/gi
正規表現を使う場合はJavascriptの正規表現の指定方法と同じく「/正規表現文字列/オプション」となります。オプションは2種類で「g」オプションはグローバルマッチ、「i」オプションは英大文字と小文字を無視です。
正規表現を書く際に「<」や「>」や「|」などコマンドラインに使う文字や空白や含むや文字列は2重引用符で囲んでください。
特殊文字「"」や「%」などを値として使う場合は、「#(16進数)」としてください。2重引用符の「"」は「#22」となります。
* Value
変更する値(正規表現対応)を指定します。例 \\$1-\\$2-\\$3
検索文字列が正規表現だった場合に後方参照など正規表現用の置換文字列を指定できます。
また、「<」や「>」や「|」などコマンドラインに使う文字や空白や含むや文字列は2重引用符で囲んでください。
特殊文字「"」や「%」などを値として使う場合は、「#(16進数)」としてください。2重引用符の「"」は「#22」となります。
固定文字列「GETVALUE」にすると、置換ではなく置換対象の値を取得します。
* CharSet
BOM無しの場合の文字コードをセットします。不要な場合は指定する必要はありません。指定しない場合は「Shift-JIS」となります。
※ファイル内の5行目以内に「encoding」や「charset」があれば指定文字コードを優先します。

# 使用例

ClickOnceの「publish.htm」ファイルの製品名が「外注受払いシステム(イントラネットワーク使用)」、タイトルが「外注受払いシステム(イントラ側ネットワーク使用)」と「側」が付いているという違いがあります。この括弧内を「工程用」に変更します。

```bat:正規表現使用
ReplaceRegex publish.htm /イントラ.?ネットワーク使用/ 工程用

rem データベース接続情報のIPアドレスを変更
ReplaceRegex "database.json" /Server=(.*?);/ 192.168.64.14; UTF-8
```

```bat:正規表現使用(括弧を後方参照でセット)
ReplaceRegex publish.htm /(\()(イントラ.?ネットワーク使用)(\))/ $1工程用$3
```


「イントラ側ネットワーク使用」で統一されていた場合

```bat:正規表現未使用
ReplaceRegex publish.htm イントラ側ネットワーク使用 工程用
```

![image](https://qiita-image-store.s3.amazonaws.com/0/14053/8ea32c53-0bdf-7306-35cb-e54917ab5b5c.png)

![image](https://qiita-image-store.s3.amazonaws.com/0/14053/ec14b3e4-38c1-eb8f-3cc6-e66d7775d59c.png)

## 値の取得
最初に見つけた「イントラ側ネットワーク使用」を取得します。

```bat
for /f "usebackq tokens=*" %%i IN (`call cscript //nologo ReplaceRegex.vbs publish.htm "/イントラ.?ネットワーク使用/" GETVALUE`) DO @set result=%%i

echo %result%
```

正規表現が使えるため括弧でグループ化するとカンマ区切りにして取得します。
例 (,イントラ側ネットワーク使用,)
※バッチでは括弧はエスケープされてしまうため、2重引用符で囲む必要があります。

```bat
for /f "usebackq tokens=*" %%i IN (`call cscript //nologo ReplaceRegex.vbs publish.htm "/(\()(イントラ.?ネットワーク使用)(\))/" GETVALUE`) DO @set result=%%i

echo %result%
```

# ソースリスト
```vb:ReplaceRegex.vbs
'--------------------------------------
'  引数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 GET_CONFIG = "GETVALUE"

Dim objArgs, fso
Dim pathName, searchPattern, valuePattern, charSet, strLine

'コマンドライン引数取得
Set objArgs = WScript.Arguments

'対象パス名取得
If objArgs.Count >= 1 Then
    pathName = objArgs(0)
    Set fso = CreateObject("Scripting.FileSystemObject")
    If fso.FileExists(pathName) = False Then
        WScript.Echo pathName & " ファイルが存在しません"
        WScript.Quit(1)
    End If
    Set fso = Nothing
Else
    WScript.Echo "対象パス名が見つかりません"
    WScript.Quit(1)
End If

'検索文字列(正規表現)取得
If objArgs.Count >= 2 Then
    searchPattern = objArgs(1)
Else
    WScript.Echo "検索文字列が見つかりません"
    WScript.Quit(2)
End If

'変更文字列(正規表現)取得
If objArgs.count >= 3 then
    valuePattern = objArgs(2)
Else
    WScript.Echo "変更文字列が見つかりません"
    WScript.Quit(3)
End If

'文字コード(BOM以外)
charSet = "SJIS"
If objArgs.count >= 4 then
    charSet = UCase(objArgs(3))
End If

'ファイル情報を取得する
Dim param, result
Set param = CreateObject("Scripting.Dictionary")
Call GetInfo(pathName, charSet)

'正規表現対応の置換処理
result = Main(pathName, searchPattern, valuePattern)
Set param = Nothing

WScript.Quit(result)

'------------------------------------------------------------------------
'  正規表現対応の置換処理
'  pathName:        対象パス名
'  searchPattern:   検索文字列(正規表現)
'  valuePattern:    変更文字列(正規表現)
'------------------------------------------------------------------------
Function Main(pathName, searchPattern, valuePattern)

    Dim reg, matches, match, stream_r, stream_pre, stream_w
    Dim bin, newtxt, line, lineSave, i, idx, count, regPattern
    Dim regexFlag, exitFlag, lfType, allLine, aryLine, result

    Main = 0

    Set stream_r = CreateObject("ADODB.Stream")
    Set stream_pre = CreateObject("ADODB.Stream")
    Set stream_w = CreateObject("ADODB.Stream")
    Set reg = CreateObject("VBScript.RegExp")
    reg.Global = True
    reg.IgnoreCase = True

    '現行の文字コードで読み込む
    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

    '改行コードで分割
    aryLine = Split(allLine, lfType)

    '検索文字列の#(16進数)の文字変換
    searchPattern = ReplaceChr(searchPattern)

    '変更文字列の#(16進数)の文字変換
    valuePattern = ReplaceChr(valuePattern)

    '検索文字列が正規表現かを確認
    regexFlag = False
    reg.Pattern =  "\/(.*)\/([gi]*)$"
    reg.Global = False
    reg.IgnoreCase = False
    Set matches = reg.Execute(searchPattern)
    regexFlag = (matches.Count > 0)
    For Each match In matches
        regPattern = match.SubMatches(0)

        Select case match.SubMatches(1)
            Case "g"
                reg.Global = True
            Case "i"
                reg.IgnoreCase = True
            Case "gi","ig"
                reg.Global = True
                reg.IgnoreCase = True
        End Select
    Next
    Set matches = Nothing

    i = 0
    count = 0
    exitFlag = False
    For idx = 0 To UBound(aryLine)
        line = aryLine(idx)
        If regexFlag = True Then
            '正規表現による置換
            reg.Pattern = regPattern
            If reg.Test(line) Then
                '置換
                result = GetValue(reg, valuePattern, line)
                line = reg.Replace(line, valuePattern)
                exitFlag = True
            End If
        Else
            '通常の置換
            result = GetValue(reg, valuePattern, line)
            lineSave = line
            line = Replace(line, searchPattern, valuePattern)
            If line <> lineSave Then exitFlag = True
        End If

        '読み込んだ行を再セット
        aryLine(idx) = line
    Next

    If exitFlag = True Then
        If UCase(valuePattern) = 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 reg = Nothing

End Function

'------------------------------------------------------------------------
'#(16進数)の文字変換
'------------------------------------------------------------------------
Function ReplaceChr(target)
    Dim reg, matches, match, hexvalue

    Set reg = CreateObject("VBScript.RegExp")
    reg.Global = True
    reg.IgnoreCase = True

    reg.Pattern =  "#([0-9A-Fa-f]{2})"
    Set matches = reg.Execute(target)
    For Each match In matches
        hexvalue = match.SubMatches(0)
        target = Replace(target, "#" & hexvalue, Chr(CInt("&H" & hexvalue)))
    Next
    Set matches = Nothing

    ReplaceChr = target

    Set reg = Nothing
End Function

'------------------------------------------------------------------------
'ファイルの文字コード等の情報を取得する
'------------------------------------------------------------------------
Function GetInfo(pathName, charSet)

    Dim stream, reg, match, matches, bytCode, allLine, line, endLine, lfType, aryLine
    Dim i, charcode

    Const FindMaxLine = 5               'BOM無し時の文字コード探索行
    GetInfo = False

    '初期値セット
    param.Add "CharSet", charSet        '文字コード
    param.Add "IsBOM", False            'BOM有無
    param.Add "LfType", "CrLf"          '改行コード
    param.Add "IsEndLf", False          '末尾改行有無

    'ファイルをオープン
    Set stream = CreateObject("ADODB.Stream")

    'BOMチェック用に先頭行を読み込む
    stream.Type = adTypeBinary
    stream.Open
    stream.LoadFromFile pathName
    bytCode = stream.Read
    stream.Close
    Call CheckBOM(bytCode)

    '文字コードを自動判定で読み込み
    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 param("LfType")
        Case "CrLf": lfType = vbCrLf
        Case "Lf": lfType = vbLf
        Case "Cr": lfType = vbCr
    End Select
    aryLine = Split(allLine, lfType)

    'BOM無しなら、encodingとcharsetの文字コード参照
    If param("IsBOM") = False Then
        Set reg = CreateObject("VBScript.RegExp")
        'encodingまたはcharsetの値を文字コードにセット
        reg.Pattern = "(?:encoding|charset).*?=(?:['""](.*?)['""]|(.*))"
        reg.IgnoreCase = True
        For i = 0 To FindMaxLine - 1
            Set matches = reg.Execute(aryLine(i))
            For Each match In matches
                charcode = UCase(match.SubMatches(0) & match.SubMatches(1))
                param("CharSet") = charcode
            Next
            If charcode > "" Or i >= UBound(aryLine) Then Exit For
        Next
        Set matches = Nothing
        Set reg = Nothing
    End If

    '最終行の改行コード有無
    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, comma, i

    GetValue = ""
    If UCase(valueString) = GET_CONFIG Then
        Set matches = reg.Execute(target)
        For Each match In matches
            If match.SubMatches.Count > 0 Then
                comma = ""
                For i=0 To match.SubMatches.Count - 1
                    GetValue = GetValue & comma & match.SubMatches(i)
                    comma = ","
                Next
            Else
                GetValue = match.Value
                Exit For
            End If
        Next
    End If

End Function
```

# ライセンスっぽいこと
* コード改変や配布は自由です。
* このツールによる義務／責任を何ら負いません。

# 最後に
「コマンドライン 正規表現 置換」で検索すると、「[mfind](http://magicant.github.io/sapps/mfind/)」や「[sakuraエディタのコマンドラインオプション](https://sakura-editor.github.io/help/HLP000109.html)」ぐらいしか見つからないのであまり需要はないかな。
プログラムは不具合があれば随時修正していきます。

このツールがお役に立てれば幸いです。