はじめに
これは、Visual Basic Advent Calendar 2017の24日目の記事となります。
設定ファイルをxml形式で作成したところ、ユーザーからIni形式で慣れているので変更して欲しいという要望がありました。
既に本体プログラムはReadXmlでDataSet型にして設定を読み込むようになっていたため、本体プログラムに出来るだけ影響しないようにIni形式をDataSet型に変換するクラスを作成しました。
説明
下記のようなiniファイルがあった場合、Dictionary型およびDataSet型にして取得できます。
特徴として、セクション名が同じで末尾数字が違った場合、同一グループとして扱います。また、キー名も同様で末尾数字が同じなら同一グループになります。
iniファイルのパースはAPIを使わず独自に行っているため、UTF-8に対応しています。
また、読み込みだけでなく書き込みも出来ます。
※内部ではini形式をXML形式に変換してDataSetのReadXmlメソッドを読んでいます。
; データベース
[DATABASE]
Host = 192.168.0.1
User = username
Pass = password
; 文字コード
[ENCODING]
enc = SJIS
; フォント情報
[FONT1]
FontType = Meiryo UI
FontSize = 36
FontColor = white
[FONT2]
FontType = MS ゴシック
FontSize = 24
FontColor = red
[FONT3]
FontType = Century Gothic
FontSize = 12
FontColor = green
; データ
[DATA1]
Code1 = 100
Str1 = Hoge
Code2 = 101
Str2 = Fuga
[DATA2]
Code1 = 200
Str1 = Hoge2
Code2 = 201
Str2 = Fuga2
読み込み
Dim path As String = "Sample.ini"
Dim iniManager As IniManager = New IniManager()
' 文字コードをUTF-8とする。
iniManager.Encording = Encoding.UTF8
' Iniファイルの読み込み。FalseはDictionary型のみ、TrueならDataSet型にも変換する。
iniManager.ReadIni(path, True)
' Dictionary型による取得
Dim host As String = iniManager.GetString("DATABASE", "Host")
' DataSet型による取得
Dim ds As DataSet = iniManager.SectionDataSet
Dim dtFont As DataTable = ds.Tables("FONT")
Dim dtData As DataTable = ds.Tables("DATA")
セクション名(DATABASE)のDictionary型の内容
セクション名(DATABASE)のDataTable型の内容
セクション名(ENCODING)のDataTable型の内容
セクション名(FONT1,FONT2,FONT3)のDataTable型の内容
同一セクション名(FONT)でまとめています。Grpの列が自動で追加され末尾数字がセットされます。末尾数字が無い場合、0 になります。
セクション名(DATA1,DATA2)のDataTable型の内容
同一セクション名(DATA)と同一キー名(Code,Str)でまとめています。GrpとIdxの列が自動で追加され末尾数字がセットされます。
書き込み
Dictionary型を指定
SetStringメソッドでセクション、キー、値をセットするとSectionDicプロパティを書き換えます。
また、SectionDicプロパティを使わず、書き換える部分のみのDictionary型を作成して指定することも出来ます。
' hostを1920.168.0.1から127.0.0.1に変更する。
iniManager.SetString("DATABASE", "Host", "127.0.0.1")
' encをSJISからUTF-8に変更する。
iniManager.SetString("ENCODING", "enc", "UTF-8")
' 書き込み
iniManager.WriteIni(path, iniManager.SectionDic)
DataTable型を指定
例ではDataTable型を指定して2回に分けて書き込みしていますが、DataSet型で一度に書き込むことが出来ます。
' データテーブルで書き換える
' FONT2のFontColorをredからblueに書き込える
dtFont.Rows(1).Item("FontColor") = "blue"
iniManager.WriteIni(path, dtFont)
' DATA2のStr2のHoge2をHellowに書き込み
dtData.Rows(1).Item("Str") = "Hellow"
iniManager.WriteIni(path, dtData)
ソースコード
IniManagerクラスを作成しています。
Visual Basic版
Imports System.IO
Imports System.Text
Imports System.Text.RegularExpressions
Public Class IniManager
' Iniファイル情報格納(OrderedDictionary型)
Public Property SectionDic() As OrderedDictionary(Of String, OrderedDictionary(Of String, String))
' Iniファイル情報格納(DataSet型)
Public Property SectionDataSet() As DataSet
' エンコーディング
Public Property Encording() As Encoding
' セクション番号区切り文字
Public Property SectionSeparator() As String
' キー番号区切り文字
Public Property KeySeparator() As String
' セクションと値の間の空白
Public Property InsertSpace() As String
' ルート名
Public Property RootName() As String
' セクショングループ用属性値
Public Const SECTION_GROUP_ATTR As String = "Grp"
' パラメーターグループ用属性値
Public Const PARAMETER_INDEX_ATTR As String = "Idx"
' XML用ルート名
Public Const DEFAULT_ROOT_NAMER As String = "Root"
' セクション用正規表現パターン
Const SECTION_PATTERN As String = "^\s*\[(?<section>[^\]]+)\].*$"
' パラメーター用正規表現パターン
Const PARAMETER_PATTERN As String = "^\s*(?<name>[^=]+)=(?<value>.*?)(\s+;(?<comment>.*))?$"
' コンストラクタ
Public Sub New()
Encording = Encoding.UTF8
SectionSeparator = ""
KeySeparator = ""
InsertSpace = " "
RootName = DEFAULT_ROOT_NAMER
End Sub
''' <summary>
''' 設定値を取得する
''' </summary>
''' <param name="section">セクション名</param>
''' <param name="keyname">キー名</param>
''' <returns> 設定値</returns>
Public Function GetString(section As String, keyname As String) As String
If Not SectionDic.ContainsKey(section) OrElse Not SectionDic(section).ContainsKey(keyname) Then Return ""
Return SectionDic(section)(keyname)
End Function
''' <summary>
''' 設定値を取得する
''' </summary>
''' <param name="keyname">キー名</param>
''' <returns>設定値</returns>
Public Function GetString(keyname As String) As String
Return GetString("", keyname)
End Function
''' <summary>
''' 設定情報を取得する
''' </summary>
''' <param name="tableName">テーブル名</param>
''' <returns>設定情報</returns>
Public Function GetTable(tableName As String) As DataTable
Return SectionDataSet.Tables(tableName)
End Function
''' <summary>
''' 設定値を更新する
''' </summary>
''' <param name="section">セクション名</param>
''' <param name="keyname">キー名</param>
''' <param name="value">設定値</param>
Public Sub SetString(section As String, keyname As String, value As String)
SectionDic(section)(keyname) = value
End Sub
''' <summary>
''' Iniファイル読み込み
''' </summary>
''' <param name="filePath">ファイルパス</param>
''' <returns>true : 正常 / false : 異常</returns>
Public Function ReadIni(filePath As String, Optional isDataSet As Boolean = False) As Boolean
' Ini情報のセット
SectionDic = GetSections(filePath)
' DataSet型に変換
If isDataSet Then Return ToDataSet()
Return True
End Function
''' <summary>
''' DataSet型に変換する
''' </summary>
''' <returns>true : 正常 / false : 異常</returns>
Public Function ToDataSet() As Boolean
SectionDataSet = New DataSet()
' XMLデータに変換する
Dim xml As String = ConvertDicToXML()
If xml = "" Then Return False
' DataSet型に変換する
Using reader As New StringReader(xml)
SectionDataSet.ReadXml(reader)
End Using
Return True
End Function
''' <summary>
''' Iniファイル情報を取得する
''' </summary>
''' <param name="filePath">ファイルパス</param>
''' <returns></returns>
Public Function GetSections(filePath As String) As OrderedDictionary(Of String, OrderedDictionary(Of String, String))
Using reader = New StreamReader(filePath, Encording)
Dim sections = New OrderedDictionary(Of String, OrderedDictionary(Of String, String))(StringComparer.Ordinal)
Dim regexSection = New Regex(SECTION_PATTERN, RegexOptions.Singleline Or RegexOptions.CultureInvariant)
Dim regexNameValue = New Regex(PARAMETER_PATTERN, RegexOptions.Singleline Or RegexOptions.CultureInvariant)
Dim currentSection = String.Empty
' セクション名が明示されていない先頭部分のセクション名を""として扱う
sections(String.Empty) = New OrderedDictionary(Of String, String)()
While True
Dim line = reader.ReadLine()
If line Is Nothing Then Exit While
' 空行は読み飛ばす
If IsBlank(line) Then Continue While
' コメント行は読み飛ばす
If line.StartsWith(";", StringComparison.Ordinal) Then
Continue While
ElseIf line.StartsWith("#", StringComparison.Ordinal) Then
Continue While
End If
Dim matchNameValue = regexNameValue.Match(line)
If matchNameValue.Success Then
' name=valueの行
sections(currentSection)(matchNameValue.Groups("name").Value.Trim()) = matchNameValue.Groups("value").Value.Trim()
Continue While
End If
Dim matchSection = regexSection.Match(line)
If matchSection.Success Then
' [section]の行
currentSection = matchSection.Groups("section").Value
If Not sections.ContainsKey(currentSection) Then
sections(currentSection) = New OrderedDictionary(Of String, String)()
End If
Continue While
End If
End While
Return sections
End Using
End Function
''' <summary>
''' Iniファイルの書き込み
''' </summary>
''' <param name="filePath">ファイルパス</param>
''' <param name="sections">更新情報</param>
''' <returns>true : 正常 / false : 異常</returns>
Public Function WriteIni(filePath As String, sections As OrderedDictionary(Of String, OrderedDictionary(Of String, String))) As Boolean
Dim result As Boolean = False
' Iniファイル情報を取得する
Dim dic = GetSections(filePath)
' 並び替えた情報を格納
Dim sortSections = New OrderedDictionary(Of String, OrderedDictionary(Of String, String))(StringComparer.Ordinal)
For Each sec In dic
For Each pair In sec.Value
If sections.ContainsKey(sec.Key) AndAlso sections(sec.Key).ContainsKey(pair.Key) Then
If Not sortSections.ContainsKey(sec.Key) Then
sortSections(sec.Key) = New OrderedDictionary(Of String, String)()
End If
sortSections(sec.Key)(pair.Key) = sections(sec.Key)(pair.Key)
End If
Next
Next
' 存在しなかったら追記
For Each sec In sections
For Each pair In sec.Value
' 既に登録済みなら何もしない
If sortSections.ContainsKey(sec.Key) AndAlso sortSections(sec.Key).ContainsKey(pair.Key) Then
Continue For
End If
' 未登録なら追加する
If Not sortSections.ContainsKey(sec.Key) Then
sortSections(sec.Key) = New OrderedDictionary(Of String, String)()
End If
sortSections(sec.Key)(pair.Key) = pair.Value
Next
Next
' セクションの最終キーを格納
Dim lastdic As New Dictionary(Of String, String)()
For Each sec In dic
If dic(sec.Key).Count > 0 Then
Dim pair = dic(sec.Key).Last()
lastdic.Add(sec.Key, pair.Key)
End If
Next
result = Write(filePath, sortSections, lastdic)
Return result
End Function
''' <summary>
''' iniファイルの書き込み
''' </summary>
''' <param name="filePath">ファイルパス</param>
''' <param name="sectionName">セクション名</param>
''' <param name="keyName">キー名</param>
''' <param name="value">値</param>
''' <returns>true : 正常 / false : 異常</returns>
Public Function WriteIni(filePath As String, sectionName As String, keyName As String, value As String) As Boolean
Dim sections = New OrderedDictionary(Of String, OrderedDictionary(Of String, String))()
sections(sectionName) = New OrderedDictionary(Of String, String)()
sections(sectionName)(keyName) = value
Return WriteIni(filePath, sections)
End Function
''' <summary>
''' Iniファイルの書き込み
''' </summary>
''' <param name="filePath">ファイルパス</param>
''' <param name="dsSections">セクション情報</param>
''' <returns>true : 正常 / false : 異常</returns>
Public Function WriteIni(filePath As String, dsSections As DataSet) As Boolean
Dim sections = New OrderedDictionary(Of String, OrderedDictionary(Of String, String))()
For Each dt As DataTable In dsSections.Tables
ToDictionary(dt, sections)
Next
Return WriteIni(filePath, sections)
End Function
''' <summary>
''' Iniファイルの書き込み
''' </summary>
''' <param name="filePath"></param>
''' <param name="dtSections"></param>
''' <returns>true : 正常 / false : 異常</returns>
Public Function WriteIni(filePath As String, dtSections As DataTable) As Boolean
If dtSections.Rows.Count = 0 Then Return False
Dim sections = New OrderedDictionary(Of String, OrderedDictionary(Of String, String))()
ToDictionary(dtSections, sections)
Return WriteIni(filePath, sections)
End Function
''' <summary>
''' Dictionary型に変換する
''' </summary>
''' <param name="dtSections">セクション情報</param>
''' <param name="sections">更新セクション情報</param>
''' <returns>true : 正常 / false : 異常</returns>
Private Function ToDictionary(dtSections As DataTable, ByRef sections As OrderedDictionary(Of String, OrderedDictionary(Of String, String))) As Boolean
For Each dr As DataRow In dtSections.Rows
Dim groupNo As String = ""
If dtSections.Columns.Contains(SECTION_GROUP_ATTR) Then
groupNo = dr(SECTION_GROUP_ATTR).ToString()
If groupNo = "0" Then groupNo = ""
End If
Dim sectionName As String = dtSections.TableName & SectionSeparator & groupNo
If sectionName = RootName Then sectionName = ""
If Not sections.ContainsKey(sectionName) Then
sections(sectionName) = New OrderedDictionary(Of String, String)()
End If
Dim indexNo As String = ""
If dtSections.Columns.Contains(PARAMETER_INDEX_ATTR) Then
indexNo = dr(PARAMETER_INDEX_ATTR).ToString()
End If
For Each column As DataColumn In dtSections.Columns
If column.ColumnName = SECTION_GROUP_ATTR OrElse column.ColumnName = PARAMETER_INDEX_ATTR OrElse column.ColumnName = RootName & "_Id" Then
Continue For
End If
Dim keyName As String = column.ColumnName & KeySeparator & indexNo
Dim keyName2 As String = keyName
If indexNo <> "" Then
Dim keyno = GetKeyAndNo(keyName, KeySeparator)
keyName2 = keyno.Item1
End If
sections(sectionName)(keyName) = dr(keyName2).ToString()
Next
Next
Return True
End Function
''' <summary>
''' iniファイルの書き込みメイン処理
''' </summary>
''' <param name="filePath">ファイルパス</param>
''' <param name="sortSections">ソート済更新情報</param>
''' <param name="lastdic">最終パラメーター情報</param>
''' <returns>true : 正常 / false : 異常</returns>
Private Function Write(filePath As String, sortSections As OrderedDictionary(Of String, OrderedDictionary(Of String, String)), lastdic As Dictionary(Of String, String)) As Boolean
Dim isSave As Boolean = False
Dim isWrite As Boolean = False
Dim sb As New StringBuilder()
Dim isExistsSection As Boolean = False
Using reader = New StreamReader(filePath, Encording)
Dim sections = New OrderedDictionary(Of String, OrderedDictionary(Of String, String))(StringComparer.Ordinal)
Dim regexSection = New Regex(SECTION_PATTERN, RegexOptions.Singleline Or RegexOptions.CultureInvariant)
Dim regexNameValue = New Regex(PARAMETER_PATTERN, RegexOptions.Singleline Or RegexOptions.CultureInvariant)
Dim currentSection = String.Empty
' セクション名が明示されていない先頭部分のセクション名を""として扱う
sections(String.Empty) = New OrderedDictionary(Of String, String)()
While True
Dim line = reader.ReadLine()
If line Is Nothing Then Exit While
' 空行は読み飛ばす
Dim isContinue As Boolean = IsBlank(line)
If Not isContinue Then
' コメント行は読み飛ばす
If line.StartsWith(";", StringComparison.Ordinal) Then
isContinue = True
ElseIf line.StartsWith("#", StringComparison.Ordinal) Then
isContinue = True
End If
' 全て終わった
If sortSections.Count = 0 Then isContinue = True
End If
If isContinue Then
sb.AppendLine(line)
Continue While
End If
' 存在しなかったら追記
Dim matchNameValue = regexNameValue.Match(line)
If sortSections.ContainsKey("") AndAlso currentSection = "" Then
isExistsSection = True
End If
If isExistsSection AndAlso matchNameValue.Success Then
Dim newline As String = line
' name=valueの行
Dim keyName As String = matchNameValue.Groups("name").Value.Trim()
If sortSections(currentSection).ContainsKey(keyName) Then
isWrite = True
Dim value As String = sortSections(currentSection)(keyName)
sortSections(currentSection).Remove(keyName)
Dim curvalue As String = matchNameValue.Groups("value").Value.Trim()
If curvalue <> "" Then
If curvalue <> value Then
' 現在値があるなら置換する
If curvalue.Contains(" ") Then
' 現在値に空白が含まれていた場合、単純な置換(キーやコメントなども置換される可能性がある)
newline = line.Replace(curvalue, value)
Else
' 現在値に空白が含まれていない場合、値のみ置換
newline = Regex.Replace(line, "(=\s+|=)([^;|\s]+)(\s+;.*|)", "${1}" & value & "${3}")
End If
' 現在値と違う値なら保存する
isSave = True
End If
Else
' = の位置に半角スペースを1つ空けて値をセット
newline = Regex.Replace(line, "(=)(\s[^;].*)", "${1}" &
InsertSpace & value & "${2}")
If value <> "" AndAlso newline.IndexOf(value) = -1 Then
newline = Regex.Replace(line, "(=)", "${1}" &
InsertSpace & value)
End If
' 現在値と違う値なら保存する
If newline = line Then isSave = True
End If
End If
' 書き換え行のセット
sb.AppendLine(newline)
' 新規パラメーターがあれば最終キー後に追記する
If lastdic(currentSection) = keyName Then
For Each s In sortSections(currentSection)
sb.AppendLine(String.Format("{0}" & InsertSpace & "=" & InsertSpace & "{1}", s.Key, s.Value))
Next
sortSections(currentSection).Clear()
End If
' セクション内のパラメーターが存在しない
If sortSections(currentSection).Count = 0 Then
' セクションを削除する
sortSections.Remove(currentSection)
End If
Continue While
End If
isExistsSection = False
Dim matchSection = regexSection.Match(line)
If matchSection.Success Then
Dim sectionName As String = matchSection.Groups("section").Value.Trim()
If sortSections.ContainsKey(sectionName) Then
currentSection = sectionName
' 対象のセクション行が存在
isExistsSection = True
End If
End If
' 行のセット
sb.AppendLine(line)
End While
End Using
' 新規セクションとパラメーターを追記する
For Each sec In sortSections
' 未登録のセクションを追加する(一行空行)
If sb.Length <> 0 Then sb.AppendLine("")
sb.AppendLine(String.Format("[{0}]", sec.Key))
isWrite = True
isSave = True
For Each pair In sec.Value
' パラメーターを追加する
sb.AppendLine(String.Format("{0}" & InsertSpace & "=" & InsertSpace & "{1}", pair.Key, pair.Value))
Next
Next
' 保存処理
If isWrite AndAlso isSave Then
File.WriteAllText(filePath, sb.ToString(), Encording)
End If
Return True
End Function
''' <summary>
''' 末尾数値分割処理
''' </summary>
''' <param name="value">対象キー</param>
''' <param name="separator">区切り文字</param>
''' <returns>分割情報</returns>
Private Function GetKeyAndNo(value As String, separator As String) As Tuple(Of String, Integer)
If Regex.IsMatch(value, "[0-9]$") Then
Dim pattern As String = "(?<Key>.*\D)(?<No>\d+$)"
If separator <> "" Then
pattern = String.Format("(?<Key>.*){0}(?<No>\d+$)", separator)
End If
Dim reg As New Regex(pattern)
Dim mat As Match = reg.Match(value)
Dim key As String = mat.Result("${Key}")
Dim no As String = mat.Result("${No}")
Return New Tuple(Of String, Integer)(key, Integer.Parse(no))
Else
Return Nothing
End If
End Function
''' <summary>
''' セクショングループ件数を取得する
''' </summary>
''' <returns>セクショングループ件数</returns>
Private Function GetSectionGroupCount() As OrderedDictionary(Of String, Integer)
Dim secCount As New OrderedDictionary(Of String, Integer)()
For Each section In SectionDic
If section.Key <> "" Then
Dim key As String = section.Key
Dim keyno = GetKeyAndNo(section.Key, SectionSeparator)
Dim cnt As Integer = 1
If keyno IsNot Nothing Then
key = keyno.Item1
If secCount.ContainsKey(key) Then
cnt = secCount(key)
cnt += 1
End If
End If
secCount(key) = cnt
End If
Next
Return secCount
End Function
''' <summary>
''' XMLデータに変換する
''' </summary>
''' <returns>XMLデータ</returns>
Private Function ConvertDicToXML() As String
If SectionDic.Count = 0 Then Return ""
Dim sb As New StringBuilder()
sb.AppendLine("<?xml version = '1.0' encoding = 'utf-8' ?>")
sb.AppendLine([String].Format("<{0}>", RootName))
' 同一名カウントチェック
Dim secCount As OrderedDictionary(Of String, Integer) = GetSectionGroupCount()
' XMLデータ生成
For Each section In SectionDic
Dim grpno As Integer = -1
Dim key As String = section.Key
Dim keyno = GetKeyAndNo(key, SectionSeparator)
If keyno IsNot Nothing Then
key = keyno.Item1
grpno = keyno.Item2
End If
' 複数存在するならグループ扱い
If grpno = -1 AndAlso key <> "" AndAlso secCount(key) > 1 Then grpno = 0
' パラメーターが連番のみかチェック
Dim isDataGrp As Boolean = True
For Each pair In section.Value
If Not Regex.IsMatch(pair.Key, "[0-9]$") Then
isDataGrp = False
Exit For
End If
Next
Dim grp As String = If(grpno > -1, String.Format(" {0}='{1}'", SECTION_GROUP_ATTR, grpno), "")
If key <> "" Then sb.Append(String.Format("<{0}{1}", key, grp))
Dim no As Integer = 0
Dim oldNo As Integer = -1
Dim pkey As String = ""
Dim isFirst As Boolean = False
For Each pair In section.Value
pkey = pair.Key
If key <> "" Then
If isDataGrp Then
keyno = GetKeyAndNo(pkey, KeySeparator)
If keyno IsNot Nothing Then
pkey = keyno.Item1
no = keyno.Item2
End If
If no <> oldNo AndAlso no > 0 Then
If isFirst Then
sb.AppendLine(" />")
sb.Append(String.Format("<{0}{1}", key, grp))
End If
sb.Append(String.Format(" {0}='{1}'", PARAMETER_INDEX_ATTR, no))
End If
oldNo = no
isFirst = True
End If
sb.Append(String.Format(" {0}='{1}'", pkey, pair.Value))
Else
' セクションが無い場合
sb.AppendLine(String.Format("<{0}>{1}</{0}>", pkey, pair.Value))
End If
Next
If key <> "" Then
sb.AppendLine(" />")
End If
Next
sb.AppendLine([String].Format("</{0}>", RootName))
Return sb.ToString()
End Function
''' <summary>
''' 空行判定
''' </summary>
''' <param name="line">対象文字列</param>
''' <returns>true:空行 / false:空行以外</returns>
Private Function IsBlank(str As String) As Boolean
' 空行(全角スペース、タブ、半角スペースのみを対象にする)
Dim re As New Regex("\s")
Dim blankline As String = re.Replace(str, "")
Return (blankline.Length = 0)
End Function
End Class
CSharp版
【2018/06/22追記】C#を追加しました。
【2018/08/25追記】VB版とC#版が違うのに気が付きました。C#版が古かった。
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
public class IniManager
{
// Iniファイル情報格納(OrderedDictionary型)
public OrderedDictionary<string, OrderedDictionary<string, string>> SectionDic { get; set; }
// Iniファイル情報格納(DataSet型)
public DataSet SectionDataSet { get; set; }
// エンコーディング
public Encoding Encording { get; set; }
// セクション番号区切り文字
public string SectionSeparator { get; set; }
// キー番号区切り文字
public string KeySeparator { get; set; }
// セクションと値の間の空白
public string InsertSpace { get; set; }
// XML用ルート名
public string RootName { get; set; }
// セクショングループ用属性値
public const string SECTION_GROUP_ATTR = "Grp";
// パラメーターグループ用属性値
public const string PARAMETER_INDEX_ATTR = "Idx";
// XML用ルート名
public const string DEFAULT_ROOT_NAMER = "Root";
// セクション用正規表現パターン
const string SECTION_PATTERN = @"^\s*\[(?<section>[^\]]+)\].*$";
// パラメーター用正規表現パターン
const string PARAMETER_PATTERN = @"^\s*(?<name>[^=]+)=(?<value>.*?)(\s+;(?<comment>.*))?$";
// コンストラクタ
public IniManager()
{
Encording = Encoding.UTF8;
SectionSeparator = "";
KeySeparator = "";
InsertSpace = " ";
RootName = DEFAULT_ROOT_NAMER;
}
/// <summary>
/// 設定値を取得する
/// </summary>
/// <param name="section">セクション名</param>
/// <param name="keyname">キー名</param>
/// <returns></returns>
public string GetString(string section, string keyname)
{
if (!SectionDic.ContainsKey(section) ||
!SectionDic[section].ContainsKey(keyname)) return "";
return SectionDic[section][keyname];
}
/// <summary>
/// 設定値を取得する
/// </summary>
/// <param name="section">セクション名</param>
/// <returns></returns>
public string GetString(string keyname)
{
return GetString("", keyname);
}
/// <summary>
/// 設定情報を取得する
/// </summary>
/// <param name="tableName">テーブル名</param>
/// <returns></returns>
public DataTable GetTable(string tableName)
{
return SectionDataSet.Tables[tableName];
}
/// <summary>
/// 設定値を更新する
/// </summary>
/// <param name="section">セクション名</param>
/// <param name="keyname">キー名</param>
/// <param name="value">設定値</param>
public void SetString(string section, string keyname, string value)
{
SectionDic[section][keyname] = value;
}
/// <summary>
/// Iniファイル読み込み
/// </summary>
/// <param name="filePath">ファイルパス</param>
/// <returns>true : 正常 / false : 異常</returns>
public bool ReadIni(string filePath, bool isDataSet = false)
{
// Ini情報のセット
SectionDic = GetSections(filePath);
// DataSet型に変換
if (isDataSet) return ToDataSet();
return true;
}
/// <summary>
/// DataSet型に変換する
/// </summary>
/// <returns>true : 正常 / false : 異常</returns>
public bool ToDataSet()
{
SectionDataSet = new DataSet();
// XMLデータに変換する
string xml = ConvertDicToXML();
if (xml == "") return false;
// DataSet型に変換する
using (StringReader reader = new StringReader(xml))
{
SectionDataSet.ReadXml(reader);
}
return true;
}
/// <summary>
/// Iniファイル情報を取得する
/// </summary>
/// <param name="filePath">ファイルパス</param>
/// <returns></returns>
public OrderedDictionary<string, OrderedDictionary<string, string>> GetSections(string filePath)
{
using (var reader = new StreamReader(filePath, Encording))
{
var sections = new OrderedDictionary<string, OrderedDictionary<string, string>>(StringComparer.Ordinal);
var regexSection = new Regex(SECTION_PATTERN, RegexOptions.Singleline | RegexOptions.CultureInvariant);
var regexNameValue = new Regex(PARAMETER_PATTERN, RegexOptions.Singleline | RegexOptions.CultureInvariant);
var currentSection = string.Empty;
// セクション名が明示されていない先頭部分のセクション名を""として扱う
sections[string.Empty] = new OrderedDictionary<string, string>();
for (;;)
{
var line = reader.ReadLine();
if (line == null)
break;
// 空行は読み飛ばす
if (IsBlank(line))
continue;
// コメント行は読み飛ばす
if (line.StartsWith(";", StringComparison.Ordinal))
continue;
else if (line.StartsWith("#", StringComparison.Ordinal))
continue;
var matchNameValue = regexNameValue.Match(line);
if (matchNameValue.Success)
{
// name=valueの行
sections[currentSection][matchNameValue.Groups["name"].Value.Trim()] = matchNameValue.Groups["value"].Value.Trim();
continue;
}
var matchSection = regexSection.Match(line);
if (matchSection.Success)
{
// [section]の行
currentSection = matchSection.Groups["section"].Value;
if (!sections.ContainsKey(currentSection))
sections[currentSection] = new OrderedDictionary<string, string>();
continue;
}
}
return sections;
}
}
/// <summary>
/// Iniファイルの書き込み
/// </summary>
/// <param name="filePath">ファイルパス</param>
/// <param name="secyions">更新情報</param>
/// <returns>true : 正常 / false : 異常</returns>
public bool WriteIni(string filePath, OrderedDictionary<string, OrderedDictionary<string, string>> sections)
{
bool result = false;
// Iniファイル情報を取得する
var dic = GetSections(filePath);
// 並び替えた情報を格納
var sortSections = new OrderedDictionary<string, OrderedDictionary<string, string>>(StringComparer.Ordinal);
foreach (var sec in dic)
{
foreach (var pair in sec.Value)
{
if (sections.ContainsKey(sec.Key) && sections[sec.Key].ContainsKey(pair.Key))
{
if (!sortSections.ContainsKey(sec.Key))
sortSections[sec.Key] = new OrderedDictionary<string, string>();
sortSections[sec.Key][pair.Key] = sections[sec.Key][pair.Key];
}
}
}
// 存在しなかったら追記
foreach (var sec in sections)
{
foreach (var pair in sec.Value)
{
// 既に登録済みなら何もしない
if (sortSections.ContainsKey(sec.Key) && sortSections[sec.Key].ContainsKey(pair.Key))
continue;
// 未登録なら追加する
if (!sortSections.ContainsKey(sec.Key))
sortSections[sec.Key] = new OrderedDictionary<string, string>();
sortSections[sec.Key][pair.Key] = pair.Value;
}
}
// セクションの最終キーを格納
Dictionary<string, string> lastdic = new Dictionary<string, string>();
foreach (var sec in dic)
{
if(dic[sec.Key].Count > 0)
{
var pair = dic[sec.Key].Last();
lastdic.Add(sec.Key, pair.Key);
}
}
//result = WriteIni(section.Key, pair.Key, pair.Value, filePath);
//if (!result) return false;
result = Write(filePath, sortSections, lastdic);
return result;
}
/// <summary>
/// iniファイルの書き込み
/// </summary>
/// <param name="filePath">ファイルパス</param>
/// <param name="sectionName">セクション名</param>
/// <param name="keyName">キー名</param>
/// <param name="value">値</param>
/// <returns>true : 正常 / false : 異常</returns>
public bool WriteIni(string filePath, string sectionName, string keyName, string value)
{
var sections = new OrderedDictionary<string, OrderedDictionary<string, string>>();
sections[sectionName] = new OrderedDictionary<string, string>();
sections[sectionName][keyName] = value;
return WriteIni(filePath, sections);
}
/// <summary>
/// Iniファイルの書き込み
/// </summary>
/// <param name="filePath">ファイルパス</param>
/// <param name="dsSections">セクション情報</param>
/// <returns>true : 正常 / false : 異常</returns>
public bool WriteIni(string filePath, DataSet dsSections)
{
var sections = new OrderedDictionary<string, OrderedDictionary<string, string>>();
foreach (DataTable dt in dsSections.Tables)
{
ToDictionary(dt, ref sections);
}
return WriteIni(filePath, sections);
}
/// <summary>
/// Iniファイルの書き込み
/// </summary>
/// <param name="filePath"></param>
/// <param name="dtSections"></param>
/// <returns>true : 正常 / false : 異常</returns>
public bool WriteIni(string filePath, DataTable dtSections)
{
if (dtSections.Rows.Count == 0) return false;
var sections = new OrderedDictionary<string, OrderedDictionary<string, string>>();
ToDictionary(dtSections, ref sections);
return WriteIni(filePath, sections);
}
/// <summary>
/// Dictionary型に変換する
/// </summary>
/// <param name="dtSections">セクション情報</param>
/// <param name="sections">更新セクション情報</param>
/// <returns>true : 正常 / false : 異常</returns>
private bool ToDictionary(DataTable dtSections, ref OrderedDictionary<string, OrderedDictionary<string, string>> sections)
{
foreach (DataRow dr in dtSections.Rows)
{
string groupNo = "";
if (dtSections.Columns.Contains(SECTION_GROUP_ATTR))
{
groupNo = dr[SECTION_GROUP_ATTR].ToString();
if(groupNo == "0") groupNo = "";
}
string sectionName = dtSections.TableName + SectionSeparator + groupNo;
if (sectionName == RootName) sectionName = "";
if(!sections.ContainsKey(sectionName))
sections[sectionName] = new OrderedDictionary<string, string>();
string indexNo = "";
if (dtSections.Columns.Contains(PARAMETER_INDEX_ATTR))
{
indexNo = dr[PARAMETER_INDEX_ATTR].ToString();
}
foreach (DataColumn column in dtSections.Columns)
{
if (column.ColumnName == SECTION_GROUP_ATTR ||
column.ColumnName == PARAMETER_INDEX_ATTR ||
column.ColumnName == RootName + "_Id") continue;
string keyName = column.ColumnName + KeySeparator + indexNo;
string keyName2 = keyName;
if(indexNo != "")
{
var keyno = GetKeyAndNo(keyName, KeySeparator);
keyName2 = keyno.Item1;
}
sections[sectionName][keyName] = dr[keyName2].ToString();
}
}
return true;
}
/// <summary>
/// iniファイルの書き込みメイン処理
/// </summary>
/// <param name="filePath">ファイルパス</param>
/// <param name="sortSections">ソート済更新情報</param>
/// <param name="lastdic">最終パラメーター情報</param>
/// <returns>true : 正常 / false : 異常</returns>
private bool Write(string filePath, OrderedDictionary<string, OrderedDictionary<string, string>> sortSections, Dictionary<string, string> lastdic)
{
bool isSave = false;
bool isWrite = false;
StringBuilder sb = new StringBuilder();
bool isExistsSection = false;
using (var reader = new StreamReader(filePath, Encording))
{
var sections = new OrderedDictionary<string, OrderedDictionary<string, string>>(StringComparer.Ordinal);
var regexSection = new Regex(SECTION_PATTERN, RegexOptions.Singleline | RegexOptions.CultureInvariant);
var regexNameValue = new Regex(PARAMETER_PATTERN, RegexOptions.Singleline | RegexOptions.CultureInvariant);
var currentSection = string.Empty;
// セクション名が明示されていない先頭部分のセクション名を""として扱う
sections[string.Empty] = new OrderedDictionary<string, string>();
for (;;)
{
var line = reader.ReadLine();
if (line == null)
break;
// 空行は読み飛ばす
bool isContinue = IsBlank(line);
if (!isContinue)
{
// コメント行は読み飛ばす
if (line.StartsWith(";", StringComparison.Ordinal))
isContinue = true;
else if (line.StartsWith("#", StringComparison.Ordinal))
isContinue = true;
// 全て終わった
if (sortSections.Count == 0)
isContinue = true;
}
if (isContinue)
{
sb.AppendLine(line);
continue;
}
// 存在しなかったら追記
var matchNameValue = regexNameValue.Match(line);
if (sortSections.ContainsKey("") && currentSection == "")
{
isExistsSection = true;
}
if (isExistsSection && matchNameValue.Success)
{
string newline = line;
// name=valueの行
string keyName = matchNameValue.Groups["name"].Value.Trim();
if (sortSections[currentSection].ContainsKey(keyName))
{
isWrite = true;
string value = sortSections[currentSection][keyName];
sortSections[currentSection].Remove(keyName);
string curvalue = matchNameValue.Groups["value"].Value.Trim();
if (curvalue != "")
{
if (curvalue != value)
{
// 現在値があるなら置換する
if(curvalue.Contains(" "))
{
// 現在値に空白が含まれていた場合、単純な置換(キーやコメントなども置換される可能性がある)
newline = line.Replace(curvalue, value);
}
else
{
// 現在値に空白が含まれていない場合、値のみ置換
newline = Regex.Replace(line, @"(=\s+|=)([^;|\s]+)(\s+;.*|)", "${1}" + value + "${3}");
}
// 現在値と違う値なら保存する
isSave = true;
}
}
else
{
// = の位置に半角スペースを1つ空けて値をセット
newline = Regex.Replace(line, @"(=)(\s[^;].*)", "${1}" + InsertSpace + value + "${2}");
if(value != "" && newline.IndexOf(value) == -1)
{
newline = Regex.Replace(line, @"(=)", "${1}" + InsertSpace + value);
}
// 現在値と違う値なら保存する
if (newline != line) isSave = true;
}
}
// 書き換え行のセット
sb.AppendLine(newline);
// 新規パラメーターがあれば最終キー後に追記する
if (lastdic[currentSection] == keyName)
{
foreach (var s in sortSections[currentSection])
{
sb.AppendLine(string.Format("{0}" + InsertSpace + "=" + InsertSpace + "{1}", s.Key, s.Value));
}
sortSections[currentSection].Clear();
}
// セクション内のパラメーターが存在しない
if (sortSections[currentSection].Count == 0)
{
// セクションを削除する
sortSections.Remove(currentSection);
}
continue;
}
isExistsSection = false;
var matchSection = regexSection.Match(line);
if (matchSection.Success)
{
string sectionName = matchSection.Groups["section"].Value.Trim();
if (sortSections.ContainsKey(sectionName))
{
currentSection = sectionName;
// 対象のセクション行が存在
isExistsSection = true;
}
}
// 行のセット
sb.AppendLine(line);
}
}
// 新規セクションとパラメーターを追記する
foreach (var sec in sortSections)
{
// 未登録のセクションを追加する(一行空行)
if (sb.Length != 0) sb.AppendLine("");
sb.AppendLine(string.Format("[{0}]", sec.Key));
isWrite = true;
isSave = true;
foreach (var pair in sec.Value)
{
// パラメーターを追加する
sb.AppendLine(string.Format("{0}" + InsertSpace + "=" + InsertSpace + "{1}", pair.Key, pair.Value));
}
}
// 保存処理
if (isWrite && isSave)
{
File.WriteAllText(filePath, sb.ToString(), Encording);
}
return true;
}
/// <summary>
/// 末尾数値分割処理
/// </summary>
/// <param name="value">対象キー</param>
/// <param name="separator">区切り文字</param>
/// <returns>分割情報</returns>
private Tuple<string, int> GetKeyAndNo(string value, string separator)
{
if (Regex.IsMatch(value, @"[0-9]$"))
{
string pattern = @"(?<Key>.*\D)(?<No>\d+$)";
if (separator != "")
pattern = string.Format(@"(?<Key>.*){0}(?<No>\d+$)", separator);
Regex reg = new Regex(pattern);
Match mat = reg.Match(value);
string key = mat.Result("${Key}");
string no = mat.Result("${No}");
return new Tuple<string, int>(key, int.Parse(no));
}
else
return null;
}
/// <summary>
/// セクショングループ件数を取得する
/// </summary>
/// <returns>セクショングループ件数</returns>
private OrderedDictionary<string, int> GetSectionGroupCount()
{
OrderedDictionary<string, int> secCount = new OrderedDictionary<string, int>();
foreach (var section in SectionDic)
{
if (section.Key != "")
{
string key = section.Key;
var keyno = GetKeyAndNo(section.Key, SectionSeparator);
int cnt = 1;
if (keyno != null)
{
key = keyno.Item1;
if (secCount.ContainsKey(key))
{
cnt = secCount[key];
cnt++;
}
}
secCount[key] = cnt;
}
}
return secCount;
}
/// <summary>
/// XMLデータに変換する
/// </summary>
/// <returns>XMLデータ</returns>
private string ConvertDicToXML()
{
if (SectionDic.Count == 0) return "";
StringBuilder sb = new StringBuilder();
sb.AppendLine("<?xml version = '1.0' encoding = 'utf-8' ?>");
sb.AppendLine(String.Format("<{0}>", RootName));
// 同一名カウントチェック
OrderedDictionary<string, int> secCount = GetSectionGroupCount();
// XMLデータ生成
foreach (var section in SectionDic)
{
int grpno = -1;
string key = section.Key;
var keyno = GetKeyAndNo(key, SectionSeparator);
if (keyno != null)
{
key = keyno.Item1;
grpno = keyno.Item2;
}
// 複数存在するならグループ扱い
if (grpno == -1 && key != "" && secCount[key] > 1) grpno = 0;
// パラメーターが連番のみかチェック
bool isDataGrp = true;
foreach (var pair in section.Value)
{
if (!Regex.IsMatch(pair.Key, @"[0-9]$"))
{
isDataGrp = false;
break;
}
}
string grp = grpno > -1 ? string.Format(" {0}='{1}'", SECTION_GROUP_ATTR , grpno) : "";
if (key != "") sb.Append(string.Format("<{0}{1}", key, grp));
int no = 0;
int oldNo = -1;
string pkey = "";
bool isFirst = false;
foreach (var pair in section.Value)
{
pkey = pair.Key;
if (key != "")
{
if (isDataGrp)
{
keyno = GetKeyAndNo(pkey, KeySeparator);
if (keyno != null)
{
pkey = keyno.Item1;
no = keyno.Item2;
}
if (no != oldNo && no > 0)
{
if (isFirst)
{
sb.AppendLine(" />");
sb.Append(string.Format("<{0}{1}", key, grp));
}
sb.Append(string.Format(" {0}='{1}'", PARAMETER_INDEX_ATTR, no));
}
oldNo = no;
isFirst = true;
}
sb.Append(string.Format(" {0}='{1}'", pkey, pair.Value));
}
else
{
// セクションが無い場合
sb.AppendLine(string.Format("<{0}>{1}</{0}>", pkey, pair.Value));
}
}
if (key != "") sb.AppendLine(" />");
}
sb.AppendLine(String.Format("</{0}>", RootName));
return sb.ToString();
}
/// <summary>
/// 空行判定
/// </summary>
/// <param name="line">対象文字列</param>
/// <returns>true:空行 / false:空行以外</returns>
private bool IsBlank(string str)
{
// 空行(全角スペース、タブ、半角スペースのみを対象にする)
Regex re = new Regex(@"\s");
string blankline = re.Replace(str, "");
return (blankline.Length == 0);
}
}
OrderedDictionary型について
IniManagerクラスの中で、次サイトの「ジェネリック版OrderedDictionary」を使用しています。読み込みだけならDictionary型で充分だったのですが、書き込みする上ではOrderedDictionaryのように順序の保証が必要でした。
ジェネリック版OrderedDictionary - smdn
このジェネリック版OrderedDictionaryは、C#版しか公開されていないのですが、SharpDevelop Ver 4.4のC#からVB.NETへのコンバート機能を使用しました。ちなみに最新版のSharpDevelop Ver 5には変換機能がつかなくなりました。
正しく変換されなかったところは次のように修正しました。
#degine ENABLE_KEYS_VALUES
↓
#Const ENABLE_KEYS_VALUES = True
Public Overloads Function Remove(key As TKey) As Boolean Implements IDictionary(Of TKey, TValue).Remove, ICollection(Of KeyValuePair(Of TKey, TValue)).Remove
↓
Public Overloads Function Remove(key As TKey) As Boolean Implements IDictionary(Of TKey, TValue).Remove
Me(index).Value
↓
Me.Items(index).Value
ライセンスっぽいこと
コード改変や配布は自由です。
このツールによる義務/責任を何ら負いません。
但し、ジェネリック版OrderedDictionary - smdnはMITライセンスとなっています。
最後に
え、C#版が欲しいって、これはVisual Basic Advent Calendarなんですよ。
ウソです、もともとC#で作成したのを、この記事用にVisual Basic用に書き換えました。そのうち、GitHubに公開します。
改良と不具合修正
【2018/08/25追記】
C#版がVB版と比べて古かったのを修正。
初期値が存在しない場合に値変更が反映されない不具合を修正しました。
InsertSpaceプロパティを追加(デフォルトは半角スペース1つ)、値書込時にセクションと値の間の半角スペース1つ固定を可変にしました。これで、半角スペース無しが設定可能です。
【2018/09/07追記】
タブのみの行があった際に別セクション扱いになってしまう不具合があり、空行判定(半角スペース、全角スペース、タブ等)を正規表現の"\s"で空文字に置換して長さ 0 なら空行扱いとするように修正しました。