class_EasyIni(sFile="", sLoadFromStr="") { return new EasyIni(sFile, sLoadFromStr) } class EasyIni { __New(sFile="", sLoadFromStr="") ; Loads ths file into memory. { this := this.CreateIniObj("EasyIni_ReservedFor_m_sFile", sFile , "EasyIni_ReservedFor_TopComments", Object()) ; Top comments can be stored in linear array because order will simply be numeric if (sFile == A_Blank && sLoadFromStr == A_Blank) return this ; Append ".ini" if it is not already there. if (SubStr(sFile, StrLen(sFile)-3, 4) != ".ini") this.EasyIni_ReservedFor_m_sFile := sFile := (sFile . ".ini") sIni := sLoadFromStr if (sIni == A_Blank) FileRead, sIni, %sFile% /* Current design (not fully implemented): --------------------------------------------------------------------------------------------------------------------------------------------------- Comments at the top of the section apply to the file as a whole. They are keyed off an internal section called "EasyIni_ReservedFor_TopComments." Comments above section headers apply to the the last key of the previous section. If a comment appears between two keys, then it will apply to the key above it -- this is consistent with the solution for comments above section headers. Newlines will be stored in similar fashion to comments. --------------------------------------------------------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------------------------------------------------------- If full-support for comments needs to be added, then the design below should supersede the design above. By saying, "Full-support" I mean a way to directly access these comments based upon sections and keys. --------------------------------------------------------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------------------------------------------------------- Comments at the top of the section apply to the file as a whole. They are keyed off an internal section called "EasyIni_ReservedFor_TopComments." Comments above section headers apply to the section header. If people dislike this, I may instead chose to make the comments apply to the last key of the previous section if there a newline in-between the comment in question and the next section. I may come up with some decent solution as I experiment If a comment appears between two keys, then it will apply to the key below it -- this is consistent with the solution for comments above section headers. Newlines will be stored in similar fashion to comments. --------------------------------------------------------------------------------------------------------------------------------------------------- */ Loop, Parse, sIni, `n, `r { sTrimmedLine := Trim(A_LoopField) ; Comments or newlines within the ini if (SubStr(sTrimmedLine, 1, 1) == ";" || sTrimmedLine == A_Blank) ; A_Blank would be a newline { ; Chr(14) is just the magical char to indicate that this line should only be a newline "`n" LoopField := A_LoopField == A_Blank ? Chr(14) : A_LoopField if (sCurSec == A_Blank) this.EasyIni_ReservedFor_TopComments.Insert(A_Index, LoopField) ; not using sTrimmedLine so as to keep comment formatting else { if (sPrevKeyForThisSec == A_Blank) ; This happens when there is a comment in the section before the first key, if any sPrevKeyForThisSec := "SectionComment" if (IsObject(this[sCurSec].EasyIni_ReservedFor_Comments)) { if (this[sCurSec].EasyIni_ReservedFor_Comments.HasKey(sPrevKeyForThisSec)) this[sCurSec].EasyIni_ReservedFor_Comments[sPrevKeyForThisSec] .= "`n" LoopField else this[sCurSec].EasyIni_ReservedFor_Comments.Insert(sPrevKeyForThisSec, LoopField) } else { if (IsObject(this[sCurSec])) this[sCurSec].EasyIni_ReservedFor_Comments := {(sPrevKeyForThisSec):LoopField} else this[sCurSec, "EasyIni_ReservedFor_Comments"] := {(sPrevKeyForThisSec):LoopField} } } continue } ; [Section] if (SubStr(sTrimmedLine, 1, 1) = "[" && InStr(sTrimmedLine, "]")) ; need to be sure that this isn't just a key starting with "[" { if (sCurSec != A_Blank && !this.HasKey(sCurSec)) this[sCurSec] := EasyIni_CreateBaseObj() sCurSec := SubStr(sTrimmedLine, 2, InStr(sTrimmedLine, "]", false, 0) - 2) ; 0 search right to left. We want to trim the *last* occurence of "]" sPrevKeyForThisSec := "" continue } ; key=val iPosOfEquals := InStr(sTrimmedLine, "=") if (iPosOfEquals) { sPrevKeyForThisSec := SubStr(sTrimmedLine, 1, iPosOfEquals - 1) ; so it's not the previous key yet...but it will be on next iteration :P val := SubStr(sTrimmedLine, iPosOfEquals + 1) StringReplace, val, val , `%A_ScriptDir`%, %A_ScriptDir%, All StringReplace, val, val , `%A_WorkingDir`%, %A_ScriptDir%, All this[sCurSec, sPrevKeyForThisSec] := val } else ; at this point, we know it isn't a comment, or newline, it isn't a section, and it isn't a conventional key-val pair. Treat this line as a key with no val { sPrevKeyForThisSec := sTrimmedLine this[sCurSec, sPrevKeyForThisSec] := "" } } ; if there is a section with no keys and it is at the bottom of the file, then we missed it if (sCurSec != A_Blank && !this.HasKey(sCurSec)) this[sCurSec] := EasyIni_CreateBaseObj() return this } CreateIniObj(parms*) { ; Define prototype object for ini arrays: static base := {__Set: "EasyIni_Set", _NewEnum: "EasyIni_NewEnum", Delete: "Delete", Remove: "EasyIni_Remove", Insert: "EasyIni_Insert", InsertBefore: "EasyIni_InsertBefore", AddSection: "EasyIni.AddSection", RenameSection: "EasyIni.RenameSection", DeleteSection: "EasyIni.DeleteSection", GetSections: "EasyIni.GetSections", FindSecs: "EasyIni.FindSecs", AddKey: "EasyIni.AddKey", RenameKey: "EasyIni.RenameKey", DeleteKey: "EasyIni.DeleteKey", GetKeys: "EasyIni.GetKeys", FindKeys: "EasyIni.FindKeys", GetVals: "EasyIni.GetVals", FindVals: "EasyIni.FindVals", HasVal: "EasyIni.HasVal", Copy: "EasyIni.Copy", Merge: "EasyIni.Merge", GetFileName: "EasyIni.GetFileName", GetOnlyIniFileName:"EasyIni.GetOnlyIniFileName", IsEmpty:"EasyIni.IsEmpty", Reload: "EasyIni.Reload", GetIsSaved: "EasyIni.GetIsSaved", Save: "EasyIni.Save", ToVar: "EasyIni.ToVar"} ; Create and return new object: return Object("_keys", Object(), "base", base, parms*) } AddSection(sec, key="", val="", ByRef rsError="") { if (this.HasKey(sec)) { rsError := "Error! Cannot add new section [" sec "], because it already exists." return false } if (key == A_Blank) this[sec] := EasyIni_CreateBaseObj() else this[sec, key] := val return true } RenameSection(sOldSec, sNewSec, ByRef rsError="") { if (!this.HasKey(sOldSec)) { rsError := "Error! Could not rename section [" sOldSec "], because it does not exist." return false } if (sOldSec = sNewSec) ; EasyIni is case-insensitve. return true ; true because the rename is harmless. this[sNewSec] := this[sOldSec] this.DeleteSection(sOldSec) return true } DeleteSection(sec) { this.Remove(sec) return } GetSections(sDelim="`n", sSort="") { for sec in this secs .= (A_Index == 1 ? sec : sDelim sec) if (sSort) Sort, secs, D%sDelim% %sSort% return secs } FindSecs(sExp, iMaxSecs="") { aSecs := [] for sec in this { if (RegExMatch(sec, sExp)) { aSecs.Insert(sec) if (iMaxSecs&& aSecs.MaxIndex() == iMaxSecs) return aSecs } } return aSecs } AddKey(sec, key, val="", ByRef rsError="") { if (this.HasKey(sec)) { if (this[sec].HasKey(key)) { rsError := "Error! Could not add key, " key " because there is a key in the same section:`nSection: " sec "`nKey: " key return false } } else { rsError := "Error! Could not add key, " key " because Section, " sec " does not exist." return false } this[sec, key] := val return true } RenameKey(sec, OldKey, NewKey, ByRef rsError="") { if (!this[sec].HasKey(OldKey)) { rsError := "Error! The specified key " OldKey " could not be modified because it does not exist." return false } ValCopy := this[sec][OldKey] this.DeleteKey(sec, OldKey) this.AddKey(sec, NewKey) this[sec][NewKey] := ValCopy return true } DeleteKey(sec, key) { this[sec].Delete(key) return } GetKeys(sec, sDelim="`n", sSort="") { for key in this[sec] keys .= A_Index == 1 ? key : sDelim key if (sSort) Sort, keys, D%sDelim% %sSort% return keys } FindKeys(sec, sExp, iMaxKeys="") { aKeys := [] for key in this[sec] { if (RegExMatch(key, sExp)) { aKeys.Insert(key) if (iMaxKeys && aKeys.MaxIndex() == iMaxKeys) return aKeys } } return aKeys } ; Non-regex, exact match on key ; returns key(s) and their assocationed section(s) FindExactKeys(key, iMaxKeys="") { aKeys := {} for sec, aData in this { if (aData.HasKey(key)) { aKeys.Insert(sec, key) if (iMaxKeys && aKeys.MaxIndex() == iMaxKeys) return aKeys } } return aKeys } GetVals(sec, sDelim="`n", sSort="") { for key, val in this[sec] vals .= A_Index == 1 ? val : sDelim val if (sSort) Sort, vals, D%sDelim% %sSort% return vals } FindVals(sec, sExp, iMaxVals="") { aVals := [] for key, val in this[sec] { if (RegExMatch(val, sExp)) { aVals.Insert(val) if (iMaxVals && aVals.MaxIndex() == iMaxVals) break } } return aVals } HasVal(sec, FindVal) { for k, val in this[sec] if (FindVal = val) return true return false } ; SourceIni: May be EasyIni object or simply a path to an ini file. ; bCopyFileName = true: Allow copying of data without copying the file name. Copy(SourceIni, bCopyFileName = true) { ; Get ini as string. if (IsObject(SourceIni)) sIniString := SourceIni.ToVar() else FileRead, sIniString, %SourceIni% ; Effectively make this function static by allowing calls via EasyIni.Copy. if (IsObject(this)) { if (bCopyFileName) sOldFileName := this.GetFileName() this := A_Blank ; avoid any copy constructor issues. ; ObjClone doesn't work consistently. It's likely a problem with the meta-function overrides, ; but this is a nice, quick hack. this := class_EasyIni(SourceIni.GetFileName(), sIniString) ; Restore file name. this.EasyIni_ReservedFor_m_sFile := sOldFileName } else return class_EasyIni(bCopyFileName ? SourceIni.GetFileName() : "", sIniString) return this } ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; /* Author: Verdlin STILL UNDER CONSTRUCTION. Need to handle comments, and then this will be complete. Function: Merge Purpose: Merge two EasyIni objects. Parameters vOtherIni: Other EasyIni object to merge with this. bRemoveNonMatching: If true, removes sections and keys that do not exist in both inis. bOverwriteMatching: If true, any key that exists in both objects will use the val from vOtherIni. vExceptionsIni: class_Easy ini object full of exceptions keys for secs. Any matching key will remain unchanged. */ Merge(vOtherIni, bRemoveNonMatching = false, bOverwriteMatching = false, vExceptionsIni = "") { ; TODO: Perhaps just save one ini, read it back in, and then perform merging? I think this would help with formatting. ; [Sections] for sec, aKeysToVals in vOtherIni { if (!this.HasKey(sec)) if (bRemoveNonMatching) this.DeleteSection(sec) else this.AddSection(sec) ; key=val for key, val in aKeysToVals { bMakeException := vExceptionsIni[sec].HasKey(key) if (this[sec].HasKey(key)) { if (bOverwriteMatching && !bMakeException) this[sec, key] := val } else { if (bRemoveNonMatching && !bMakeException) this.DeleteKey(sec, key) else if (!bRemoveNonMatching) this.AddKey(sec, key, val) } } } return } ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; /* Author: Verdlin Function: GetFileName Purpose: Wrapper to return the extremely long named member var, EasyIni_ReservedFor_m_sFile Parameters None */ GetFileName() { return this.EasyIni_ReservedFor_m_sFile } ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; /* Author: Verdlin Function: GetFileName Purpose: Wrapper to return just the .ini name without the path. Parameters None */ GetOnlyIniFileName() { return SubStr(this.EasyIni_ReservedFor_m_sFile, InStr(this.EasyIni_ReservedFor_m_sFile,"\", false, -1)+1) } ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; /* Author: Verdlin Function: IsEmpty Purpose: To indicate whether or not this ini has data Parameters None */ IsEmpty() { return (this.GetSections() == A_Blank ; No sections. && !this.EasyIni_ReservedFor_TopComments.HasKey(1)) ; and no comments. } ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; /* Author: Verdlin Function: Reload Purpose: Reloads object from ini file. This is necessary when other routines may be modifying the same ini file. Parameters None */ Reload() { if (FileExist(this.GetFileName())) this := class_EasyIni(this.GetFileName()) ; else nothing to reload. return this } ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; TODO: Add option to store load and save times in comment at bottom of ini? Save(sSaveAs="", bWarnIfExist=false) { if (sSaveAs == A_Blank) sFile := this.GetFileName() else { sFile := sSaveAs ; Append ".ini" if it is not already there. if (SubStr(sFile, StrLen(sFile)-3, 4) != ".ini") sFile .= ".ini" if (bWarnIfExist && FileExist(sFile)) { MsgBox, 4,, The file "%sFile%" already exists.`n`nAre you sure that you want to overwrite it? IfMsgBox, No return false } } ; Formatting is preserved in ini object. FileDelete, %sFile% bIsFirstLine := true for k, v in this.EasyIni_ReservedFor_TopComments { FileAppend, % (A_Index == 1 ? "" : "`n") (v == Chr(14) ? "" : v), %sFile% bIsFirstLine := false } for section, aKeysToVals in this { FileAppend, % (bIsFirstLine ? "[" : "`n[") section "]", %sFile% bIsFirstLine := false bEmptySection := true for key, val in aKeysToVals { bEmptySection := false FileAppend, `n%key%=%val%, %sFile% ; Add the comment(s) for this key sComments := this[section].EasyIni_ReservedFor_Comments[key] Loop, Parse, sComments, `n FileAppend, % "`n" (A_LoopField == Chr(14) ? "" : A_LoopField), %sFile% } if (bEmptySection) { ; An empy section may contain comments... sComments := this[section].EasyIni_ReservedFor_Comments["SectionComment"] Loop, Parse, sComments, `n FileAppend, % "`n" (A_LoopField == Chr(14) ? "" : A_LoopField), %sFile% } } return true } ToVar() { sTmpFile := "$$$EasyIni_Temp.ini" this.Save(sTmpFile, !A_IsCompiled) FileRead, sIniAsVar, %sTmpFile% FileDelete, %sTmpFile% return sIniAsVar } } ; For all of the EasyIni_* functions below, much credit is due to Lexikos and Rbrtryn for their work with ordered arrays ; See http://www.autohotkey.com/board/topic/61792-ahk-l-for-loop-in-order-of-key-value-pair-creation/?p=389662 for Lexikos's initial work with ordered arrays ; See http://www.autohotkey.com/board/topic/94043-ordered-array/#entry592333 for Rbrtryn's OrderedArray lib EasyIni_CreateBaseObj(parms*) { ; Define prototype object for ordered arrays: static base := {__Set: "EasyIni_Set", _NewEnum: "EasyIni_NewEnum", Delete: "Delete", Remove: "EasyIni_Remove", Insert: "EasyIni_Insert", InsertBefore: "EasyIni_InsertBefore"} ; Create and return new base object: return Object("_keys", Object(), "base", base, parms*) } EasyIni_Set(obj, parms*) { ; If this function is called, the key must not already exist. ; Sub-class array if necessary then add this new key to the key list, if it doesn't begin with "EasyIni_ReservedFor_" if parms.maxindex() > 2 ObjInsert(obj, parms[1], EasyIni_CreateBaseObj()) ; Skip over member variables if (SubStr(parms[1], 1, 20) <> "EasyIni_ReservedFor_") ObjInsert(obj._keys, parms[1]) ; Since we don't return a value, the default behaviour takes effect. ; That is, a new key-value pair is created and stored in the object. } EasyIni_NewEnum(obj) { ; Define prototype object for custom enumerator: static base := Object("Next", "EasyIni_EnumNext") ; Return an enumerator wrapping our _keys array's enumerator: return Object("obj", obj, "enum", obj._keys._NewEnum(), "base", base) } EasyIni_EnumNext(e, ByRef k, ByRef v="") { ; If Enum.Next() returns a "true" value, it has stored a key and ; value in the provided variables. In this case, "i" receives the ; current index in the _keys array and "k" receives the value at ; that index, which is a key in the original object: if r := e.enum.Next(i,k) ; We want it to appear as though the user is simply enumerating ; the key-value pairs of the original object, so store the value ; associated with this key in the second output variable: v := e.obj[k] return r } EasyIni_Remove(obj, parms*) { r := ObjRemove(obj, parms*) ; Remove keys from main object Removed := [] for k, v in obj._keys ; Get each index key pair if not ObjHasKey(obj, v) ; if key is not in main object Removed.Insert(k) ; Store that keys index to be removed later for k, v in Removed ; For each key to be removed ObjRemove(obj._keys, v, "") ; remove that key from key list return r } EasyIni_Insert(obj, parms*) { r := ObjInsert(obj, parms*) ; Insert keys into main object enum := ObjNewEnum(obj) ; Can't use for-loop because it would invoke EasyIni_NewEnum while enum[k] { ; For each key in main object for i, kv in obj._keys ; Search for key in obj._keys if (k = "_keys" || k = kv || SubStr(k, 1, 20) = "EasyIni_ReservedFor_" || SubStr(kv, 1, 20) = "EasyIni_ReservedFor_") ; If found... continue 2 ; Get next key in main object ObjInsert(obj._keys, k) ; Else insert key into obj._keys } return r } EasyIni_InsertBefore(obj, key, parms*) { OldKeys := obj._keys ; Save key list obj._keys := [] ; Clear key list for idx, k in OldKeys { ; Put the keys before key if (k = key) ; back into key list break obj._keys.Insert(k) } r := ObjInsert(obj, parms*) ; Insert keys into main object enum := ObjNewEnum(obj) ; Can't use for-loop because it would invoke EasyIni_NewEnum while enum[k] { ; For each key in main object for i, kv in OldKeys ; Search for key in OldKeys if (k = "_keys" || k = kv) ; If found... continue 2 ; Get next key in main object ObjInsert(obj._keys, k) ; Else insert key into obj._keys } for i, k in OldKeys { ; Put the keys after key if (i < idx) ; back into key list continue obj._keys.Insert(k) } return r }