Page 1 of 2

Function: IniRead()

Posted: 07 Mar 2019, 09:18
by megnatar
Rewritten function iniRead()

Well I've started this to reduce the nr of lines in my scripts. However I ended up with a function 75 lines long. So much for my reasoning, however this is mostly because iniRead() can be used in a flexible way. It can load all, one or a couple sections. Empty and commented lines are ignored.

If you wonder how? Because variables are local to functions, the exaption is. Referenced variables are not local to functions. So %VarRef% represents global variables in which some value is stored. %VarRef% := "ValueOfVar"

Parameters info.
InputFile:
Should be the name or full path and name to the ini file to get the variables from.

LoadSection:
When empty, all variables from the file will be loaded. Otherwise LoadSection can be a single sectionname or a couple of sectionnames, each divided by a space. The order in which there stored does not matter. LoadSection can also be a object holding the names to load.
  • String: "Section"
  • Strings: "Section3 Section2 Section5"
  • Object: ["Section3", "Section2", "Section5"]

By Megnatar.

Might be useful to others...

Code: Select all

/*
    Well I've started this to reduce the nr of lines in my scripts. However I ended up with a function 75 lines long.
    So much for my reasoning, however this is mostly because iniRead() can be used in a flexible way. It can load
    all, one or a couple sections. The order in which there stored does not matter. Empty and commented lines are ignored.
    
    If you wonder how? Because variables are local to functions, the exaption is.
    Referenced variables are not local to functions. So %VarRef% represents global variables in which some value is stored 
    
    %VarRef% := "ValueOfVar"        
    
    Parameters info.
    InputFile:          Should be the name or path to the ini file to get the variables from.
    LoadSection:        When empty, all variables from the file will be loaded. Otherwise LoadSection can be
                        a single sectionname or a couple of sectionnames, each devided by a space.
                        LoadSection can also be a object holding the names to load.
                        
                        String:     "Section"
                        Strings:    "Section3 Section2 Section5"
                        Object:     ["Section3", "Section2", "Section5"]
    
    By Megnatar.
*/

iniRead(InputFile, LoadSection = 0) {
    if (LoadSection) {
        if (IsObject(LoadSection)) {                                            ; Load multiple sections using a object
             for i, Name in LoadSection
             {
                Loop, parse, % FileOpen(InputFile, 0).read(), `n, `r
                {
                    if (InStr(A_Loopfield, Name)) {
                        SectionName := StrReplace(A_Loopfield, "["), SectionName := StrReplace(SectionName, "]")
                        Continue
                    }
                    if (SectionName) {
                        if (((InStr(A_LoopField, "[",, 1, 1)) = 1) | ((InStr(A_LoopField, "`;",, 1, 1)) = 1) | (!A_LoopField)) {
                            if (((InStr(A_LoopField, "`;",, 1, 1)) = 1) | (!A_LoopField)) {
                                Continue
                            } else {
                                SectionName := ""
                                break
                            }
                        }
                        VarRef := SubStr(A_LoopField, 1, InStr(A_LoopField, "=")-1), %VarRef% := SubStr(A_LoopField, InStr(A_LoopField, "=")+1)
                    }
                }
            }
        } else if (!IsObject(LoadSection)) {
            if ((InStr(LoadSection, " ")) > 1) {                                ; Load multiple sections using strings
                Sections := []
                Loop, Parse, LoadSection, " ", A_Space
                    Sections[A_Index] := A_Loopfield
                for i, Name in Sections
                {
                    Loop, parse, % FileOpen(InputFile, 0).read(), `n, `r
                    {
                        if (InStr(A_Loopfield, Name)) {
                            SectionName := StrReplace(A_Loopfield, "["), SectionName := StrReplace(SectionName, "]")
                            Continue
                        }
                        if (SectionName) {
                            if (((InStr(A_LoopField, "[",, 1, 1)) = 1) | ((InStr(A_LoopField, "`;",, 1, 1)) = 1) | (!A_LoopField)) {
                                if (((InStr(A_LoopField, "`;",, 1, 1)) = 1) | (!A_LoopField)) {
                                    Continue
                                } else {
                                    SectionName := ""
                                    break
                                }
                            }
                            VarRef := SubStr(A_LoopField, 1, InStr(A_LoopField, "=")-1), %VarRef% := SubStr(A_LoopField, InStr(A_LoopField, "=")+1)
                        }
                    }
                }
            } Else {                                                        ; Load single section
                Loop, parse, % FileOpen(InputFile, 0).read(), `n, `r
                {
                    if (InStr(A_Loopfield, LoadSection)) {
                        SectionName := StrReplace(A_Loopfield, "["), SectionName := StrReplace(SectionName, "]")
                        Continue
                    }
                    If (SectionName) {
                        if ((InStr(A_LoopField, "[",, 1, 1)) = 1)
                            Break
                        VarRef := SubStr(A_LoopField, 1, InStr(A_LoopField, "=")-1), %VarRef% := SubStr(A_LoopField, InStr(A_LoopField, "=")+1)        
                    }
                }
            }
        }
    } else if (!LoadSection) {                                              ; Load all variables from ini
        Loop, parse, % FileOpen(InputFile, 0).read(), `n, `r
        {
            if (((InStr(A_LoopField, "[",, 1, 1)) = 1) | ((InStr(A_LoopField, "`;",, 1, 1)) = 1) | (!A_LoopField))
                Continue
            VarRef := SubStr(A_LoopField, 1, InStr(A_LoopField, "=")-1), %VarRef% := SubStr(A_LoopField, InStr(A_LoopField, "=")+1)
        }
    }
    Return
}

Re: Function: IniRead()

Posted: 07 Mar 2019, 10:02
by swagfag
ignoring the fact that dumping vars into the global(or whatever scope, for that matter) scope probably isnt the way to go about this, how are these variables ure creating made accessible outside the function?
or are u suggesting people write their scripts inside the function, after the loop?

Re: Function: IniRead()

Posted: 07 Mar 2019, 10:54
by megnatar
because of the variable reference - %TempVar% - that is used in the loop.

And because of this:
Functions Local
•A dynamic variable reference may resolve to an existing global variable if no local variable exists by that name.

Here test it!

Code: Select all

/* 
[ScriptGui]
; comment should be here
SomeObject=MyObj[]
Gui_X=387
Gui_Y=144
Gui_W=160
Gui_H=28
[ScriptVars]
Tabnr=1
ClientApp=Q:\FileIsHere\ThisFile.exe
Folder=Q:\Some\Path\To\Here

*/
#NoEnv

ReadIni(A_ScriptFullPath)
MsgBox % ""
. "Gui_X = " Gui_X "`n"
. "Gui_Y = " Gui_Y "`n"
. "Gui_W = " Gui_W "`n"
. "Gui_H = " Gui_H "`n"
. "Tabnr = " Tabnr "`n"
. "ClientApp = " ClientApp "`n"
. "Folder = " Folder "`n"
. "SomeObject = " SomeObject "`n"
ExitApp

ReadIni(InputFile) {
    local TmpVar                                                   

    Loop, parse, % FileOpen(InputFile, 0).read(), `n, `r "/*"
    {
        if (((InStr(A_LoopField, "[")) = 1 ? 1) || ((InStr(A_LoopField, "`;")) = 1 ? 1) || !A_LoopField)
            Continue
        If (A_Index = 13)
            Break
            
        TmpVar := SubStr(A_LoopField, 1, InStr(A_LoopField, "=")-1)
        %TmpVar% := SubStr(A_LoopField, InStr(A_LoopField, "=")+1) 
    }                                                              
}

Re: Function: IniRead()

Posted: 07 Mar 2019, 11:15
by just me
INI-files are usually divided into sections for a reason.

What will your function do with

Code: Select all

[Edit1]
X=10
Y=10
W=100
H=20
[ListView1]
X=10
Y=40
W=300
H=100
?

And what happens with comment lines?

Re: Function: IniRead()

Posted: 07 Mar 2019, 11:19
by swagfag
A dynamic variable reference may resolve to an existing global variable if no local variable exists by that name.
everyday we stray further from god's light

Re: Function: IniRead()

Posted: 07 Mar 2019, 12:14
by megnatar
just me wrote:
07 Mar 2019, 11:15
INI-files are usually divided into sections for a reason.

What will your function do with

Code: Select all

[Edit1]
X=10
Y=10
W=100
H=20
[ListView1]
X=10
Y=40
W=300
H=100
?

And what happens with comment lines?
Yeah it's true, the function can only be successful when the file is read from path. I could filter that though, let it read the contant from var.
Sections will be ignored, it imports the whole ini. Sections wont exist as variables..

Comments?
Good point. They will probably be stored as part of, or as a variable depending on where the comment is placed.
I will fix that!

Re: Function: IniRead()

Posted: 07 Mar 2019, 12:33
by toralf
You are checking for a section by looking for a [. But the [ could be the value of a var. it would be better if you check if [ is the first character of that line. And maybe also check if there is a closing ].

Re: Function: IniRead()

Posted: 07 Mar 2019, 13:14
by megnatar
toralf wrote:
07 Mar 2019, 12:33
You are checking for a section by looking for a [. But the [ could be the value of a var. it would be better if you check if [ is the first character of that line. And maybe also check if there is a closing ].
Fixed!
Thanx for the good comment!
You have any more "what if's" available?

Re: Function: IniRead()

Posted: 07 Mar 2019, 13:22
by just me
If you look a bit closer at my example you see that the keys X, Y, W, and H will be read twice with partially different values. AFAICS, only the second value will be available after the function call.

Re: Function: IniRead()

Posted: 19 Mar 2019, 12:30
by megnatar
just me wrote:
07 Mar 2019, 13:22
If you look a bit closer at my example you see that the keys X, Y, W, and H will be read twice with partially different values. AFAICS, only the second value will be available after the function call.

Yes i see that local variables are currently not supported. Although, the only situation I can think of when only loading one section. Is when a other function ore class uses IniRead. I don't know about others but I rarely read ini's from there. But for the sake of compatibility I will make it so that a array is returned by IniRead when loading a single section. When I have time.....

Re: Function: IniRead()

Posted: 20 Mar 2019, 13:22
by nou
What about this code? Run it, and then call the variables by doing: section.key

note: For some reason, when I make an associative array in the function using

Code: Select all

%sectionList%[key] := value
I'm able to find the variable outside the function. So if my ini file was like:

Code: Select all

[Edit1]
X=10 ; comments on side? 
Y=10
W=100
H=20	; randomly random comments. 
[ListView1]
X=10
Y=40
; more pseudo comments? 
W=300
H=100




M = 34





L= 85
I can find the value OUTSIDE the function by just doing this:

Code: Select all

iniRead("test.ini")
MsgBox % Listview1.W
Here's the code to the function I use

Code: Select all

; Function here: 
IniRead(filename)
{
; ************************* Read File *************************

	iniFile := FileOpen(filename, "r")
	ini := iniFile.read()
	ini.close()

; ********************* String Replace ************************

	; gives every comment it's own new line
	ini := StrReplace(ini, ";", "`r`n;")

	; removes all lines that starts with ; 
	ini := RegExReplace(ini, ";.*")

	; replaces all (`r`n) with (`n)
	ini := StrReplace(ini, "`r`n", "`n")

	; removes all doublespacings
	outCount := 1
	While (outCount != 0)
		ini := StrReplace(ini, "`n`n", "`n", OutCount)

; *************************************************************

	; splits by the `n
	arrayIni := StrSplit(ini, "`n")

	; makes an array to keep track of our Sections
	SectionList := []

; ********************* Getting the Info **********************

	; enumerate array + variables
	for key, value in arrayIni
		{
			; Regex search for starting "[" and ending "]"
			RegExMatch(trim(value), "^\[.*?]$", checkSection)
			if (trim(value) = checkSection)
			{
				; remove the first and last characters, the brackets. 
				section := substr(checkSection, 2, -1)

				; adds this into our list of Sections
				SectionList.push(section)

				; if found, create a new array with that as an array name. 
				%section% := {}
			}
			else 
				{
					; splits the line by the "=" sign. 
					newKey := StrSplit(value, "=")

					; stores the left + right values into our array, respectively.
					%section%[TRIM(newKey.1)] := TRIM(newKey.2)
				}

		}
; *************************************************************

	;~ ; check our arrays
	;~ 		str := ""

	;~ ; check our array
	;~ 	for index, Section in SectionList
	;~ 	{
	;~ 		; checks the name of the section
	;~ 		str .= Index ": " Section "`n"

	;~ 		; checks each section's key + value
	;~ 		for key, value in %Section%
	;~ 			str .= key ": " value "`n"

	;~ 		str .= "`n"
	;~ 	}

	;~ 	MsgBox % str

	return SectionList
}

Re: Function: IniRead()

Posted: 21 Mar 2019, 01:25
by lblb
Reminds me of the conceptually similar globalsFromIni() from the old forum that creates global variables:

https://autohotkey.com/board/topic/25515-globalsfromini-creates-globals-from-an-ini-file/

Re: Function: IniRead()

Posted: 22 Mar 2019, 10:35
by megnatar
nou wrote:
20 Mar 2019, 13:22
What about this code? Run it, and then call the variables by doing: section.key

note: For some reason, when I make an associative array in the function using

Code: Select all

%sectionList%[key] := value
I'm able to find the variable outside the function. So
............
That's a good idea, to reference the section as 'attached variable'. why didn't I think of that. Honestly I was not to fond of the idea of returning a array. After all, the whole idea was to quickly load some variables from a ini. I will change it in due time...

Re: Function: IniRead()

Posted: 22 Mar 2019, 11:58
by nou
XD Don't think of it as "returning it as an array". think of it as a "Section.key" variable, as oppose to a "sectionkey" variable. You know, a variable with an extra period in between. Otherwise, without any separator between the section and key, you might run into issues when you do something like this:

Code: Select all

[Setting]
s1=25

[Settings]
1=3

Re: Function: IniRead()

Posted: 26 Mar 2019, 01:46
by freakkk
Here is a simple ini file class I used to use before I started to upgrade my data storage to json:

Code: Select all

  ;-- read values of ini file into obj
  oIni := new IniFileObj( A_ScriptDir "\MyIniFile1.ini" )

  ;-- If file doesn't exist, create sections and add keys to it
  If !FileExist( A_ScriptDir "\MyIniFile1.ini" ){
    oIni.section1 := {}
    oIni.section1.key1 := "section 1, key 1"
    oIni.section1.key2 := "section 1, key 2"
    oIni.section2 := {}
    oIni.section2.key1 := "section 2, key 1"
    oIni.section2.key2 := "section 2, key 2"
    oIni.Save()    ; save to file
  }

  ; MsgBox, % oIni.section1.key1
  MsgBox, % Obj2String( oIni )   ;- display all values

  ;-- Create ini file obj, set to save settings on exit
  oIni := new IniFileObj( A_ScriptDir "\MyIniFile2.ini", true )
  If !FileExist(A_ScriptDir "\MyIniFile2.ini")
    oIni.section3 := { key1: "this is section three, key one.." }

ExitApp


class IniFileObj {
  __New( filePath, saveOnExit:=false ){
    this.filePath := filePath
    while thisLine := StrSplit( FileOpen(filePath,"r").Read(), "`n", "`r" )[ A_Index ]
      If RegExMatch(thisLine,"((\[(?<Section>.*)\])|(?<Key>\w+)=(?<Value>.*)$)",_)
        _Key ? this[thisSection].Insert(_key,_value) : "", _Section ? (thisSection:=_Section, this[thisSection]:=[] )
    saveOnExit ? OnExit( ObjBindMethod(this, "Save") )      
  }
  Save(){
    For section, objSectionKeys in this
      For key, value in objSectionKeys
        IniWrite, % value, % this.filePath, % section, % key
  }
}

Obj2String(o,ind="",indChar="     ")  {
  For i, v in o
    l .= (l ? "`r`n" : "") ind i " - " v, l .= IsObject(v) ? "`r`n" %A_ThisFunc%(v, ind indChar, indChar) : ""
  return l
}

Re: Function: IniRead()

Posted: 26 May 2019, 15:04
by toralf
Dear freakkk,

Thanks for posting the class.

If I understand correctly the class can’t have a section called filepath, is that correct?

Do you also have a Version that supports default values? E.g. by providing an default {section:{key:value}} object for the __New() method.

Is the a reason you use iniwrite on save() instead of filewrite or fileappend?

Re: Function: IniRead()

Posted: 26 May 2019, 15:43
by toralf
Three more questions
Does the while loop read the file completely each iteration? That seams to be an overdose of doing almost everything on one line
You regex does not handle comments in the ini, is that correct?
And the while loop will stop on empty lines...

I do not want to criticize you. I appreciate what you have created and that you share it. I consider to use it myself. But before I want to understand it’s limits or get into a discussion on how these could be overcome.

Re: Function: IniRead()

Posted: 27 May 2019, 06:46
by toralf
Hi there,

This would be my mod of the class.
Basically, I extended it with handling default values, changed the While to a For Loop and modified the RegEx to handle comments correctly. Thus, I hope it is more robust against human interaction with the ini file.

Code: Select all

  IniFileName := A_ScriptDir "\MyIniFile1.ini"
  If !FileExist( IniFileName ){
    Content = 
(
[Section1]
Key1=Section 1, Key 1
Key2=Section 1, Key 2
;comment with [Section Name]
;[Commented Section]
  ;  comment with [Commented Section Name]
  ;  [Commented Section indented]
   [Section2]
Key1=Section 2, Key 1
Key2=Section 2, Key 2
Key3=Text with = as a test
;Key4=a comment
  ;  Key5=a comment indented
  ;  comment = with equal sign
Key6 =  Spaces around =
  Key7=Key indented
)
  FileAppend, %Content%, %IniFileName%
  }

  DefaultValues := {Section1:{Key2: "Value exists in INI"
                            , Key4: "Value from Default"}
                 , "Section_test":{Key1: A_ScriptDir
                                 , 4: A_USerName }}

  oIni := new IniFileObj( IniFileName, DefaultValues )
  MsgBox, % Obj2String( oIni )   ;- display all values

  oIni := new IniFileObj( A_ScriptDir "\MyIniFile2.ini", ,true )
  If !FileExist(A_ScriptDir "\MyIniFile2.ini")
    oIni.section3 := { key1: "this is section three, key one.." }
  MsgBox, % Obj2String( oIni )   ;- display all values
  ExitApp
  
class IniFileObj {
  __New( filePath, DefaultValues := "", saveOnExit:=false ){
    For Section, KeysAndValues in DefaultValues {
      this[Section]:=[]
      For key, value in KeysAndValues
        this[Section].Insert(key,value)
    }
    this.filePath := filePath
    For i, thisLine in StrSplit( FileOpen(filePath,"r").Read(), "`n", "`r" ) 
      If RegExMatch(thisLine,"^\s*(?<!;)((\[(?<Section>.*?)\])|(?<Key>\w+)\s*=(?<Value>.*)$)",_)
        _Key ? this[thisSection].Insert(_key,_value) : "", _Section ? (thisSection:=_Section, !this.Haskey(thisSection) ? this[thisSection]:=[]) 
    saveOnExit ? OnExit( ObjBindMethod(this, "Save") ) 
  }
  Save(){
    For Section, KeysAndValues in this
      For key, value in KeysAndValues
        IniWrite, % value, % this.filePath, % Section, % key
  }
}

Obj2String(o,ind="",indChar="     ")  {
  For i, v in o
    l .= (l ? "`r`n" : "") ind i " - " v, l .= IsObject(v) ? "`r`n" %A_ThisFunc%(v, ind indChar, indChar) : ""
  return l
}

Re: Function: IniRead()

Posted: 17 Jul 2019, 17:42
by Adventurer
toralf wrote:
27 May 2019, 06:46
Hi there,

This would be my mod of the class.
Basically, I extended it with handling default values, changed the While to a For Loop and modified the RegEx to handle comments correctly. Thus, I hope it is more robust against human interaction with the ini file.

Code: Select all

  IniFileName := A_ScriptDir "\MyIniFile1.ini"
  If !FileExist( IniFileName ){
    Content = 
(
[Section1]
Key1=Section 1, Key 1
Key2=Section 1, Key 2
;comment with [Section Name]
;[Commented Section]
  ;  comment with [Commented Section Name]
  ;  [Commented Section indented]
   [Section2]
Key1=Section 2, Key 1
Key2=Section 2, Key 2
Key3=Text with = as a test
;Key4=a comment
  ;  Key5=a comment indented
  ;  comment = with equal sign
Key6 =  Spaces around =
  Key7=Key indented
)
  FileAppend, %Content%, %IniFileName%
  }

  DefaultValues := {Section1:{Key2: "Value exists in INI"
                            , Key4: "Value from Default"}
                 , "Section_test":{Key1: A_ScriptDir
                                 , 4: A_USerName }}

  oIni := new IniFileObj( IniFileName, DefaultValues )
  MsgBox, % Obj2String( oIni )   ;- display all values

  oIni := new IniFileObj( A_ScriptDir "\MyIniFile2.ini", ,true )
  If !FileExist(A_ScriptDir "\MyIniFile2.ini")
    oIni.section3 := { key1: "this is section three, key one.." }
  MsgBox, % Obj2String( oIni )   ;- display all values
  ExitApp
  
class IniFileObj {
  __New( filePath, DefaultValues := "", saveOnExit:=false ){
    For Section, KeysAndValues in DefaultValues {
      this[Section]:=[]
      For key, value in KeysAndValues
        this[Section].Insert(key,value)
    }
    this.filePath := filePath
    For i, thisLine in StrSplit( FileOpen(filePath,"r").Read(), "`n", "`r" ) 
      If RegExMatch(thisLine,"^\s*(?<!;)((\[(?<Section>.*?)\])|(?<Key>\w+)\s*=(?<Value>.*)$)",_)
        _Key ? this[thisSection].Insert(_key,_value) : "", _Section ? (thisSection:=_Section, !this.Haskey(thisSection) ? this[thisSection]:=[]) 
    saveOnExit ? OnExit( ObjBindMethod(this, "Save") ) 
  }
  Save(){
    For Section, KeysAndValues in this
      For key, value in KeysAndValues
        IniWrite, % value, % this.filePath, % Section, % key
  }
}

Obj2String(o,ind="",indChar="     ")  {
  For i, v in o
    l .= (l ? "`r`n" : "") ind i " - " v, l .= IsObject(v) ? "`r`n" %A_ThisFunc%(v, ind indChar, indChar) : ""
  return l
}
Spaces in the key name break it, e.g. 'key1' works but 'key 1' doesn't.

Cap'n Odin on Discord showed me how to fix this.

Code: Select all

;Code to replace:
If RegExMatch(thisLine,"^\s*(?<!;)((\[(?<Section>.*?)\])|(?<Key>\w+)\s*=(?<Value>.*)$)",_)

;Option #1 - This allows letters, numbers, and spaces to match:
If RegExMatch(thisLine,"^\s*(?<!;)((\[(?<Section>.*?)\])|(?<Key>(\w| )+?)\s*=(?<Value>.*)$)",_)

;Option #2 - This allows anything that isn't the equal sign to match:
If RegExMatch(thisLine,"^\s*(?<!;)((\[(?<Section>.*?)\])|(?<Key>.+?)\s*=(?<Value>.*)$)",_)

Re: Function: IniRead()

Posted: 29 Dec 2019, 15:55
by freakkk
toralf wrote:
26 May 2019, 15:04
Dear freakkk,
Well first of all, toralf it's wonderful to see your name still active on the forum! I've learned a lot and become inspired by a number of things that you have posted before in the past, so I feel honored that you were taking a look at my code. Somehow I never saw the notification that you responded to my post until now.

toralf wrote:
27 May 2019, 06:46
This would be my mod of the class.
I'm glad you were able to modify it to fit your needs. I'll most likely replace my version with your modified version, but I honestly barely use ini files anymore (see also JsonFile).
toralf wrote:
26 May 2019, 15:04
Do you also have a Version that supports default values? E.g. by providing an default {section:{key:value}} object for the __New() method.
That is a very good point. I created that library many years ago, and haven't looked at it since. I've gotten much better at writing classes since then, and looking back realize how incomplete this was.
toralf wrote:
26 May 2019, 15:04
Is the a reason you use iniwrite on save() instead of filewrite or fileappend?
If I were recreating this library now, I would definately be doing a single filewrite rather than that approach!
toralf wrote:
26 May 2019, 15:43
You regex does not handle comments in the ini, is that correct?
I've never actually used (or seen.??) comments in ini files, so I never thought to handle that. I'm glad your mod handles comments!

Apologies for not responding back to you sooner toralf!