Page 1 of 1

Function: IniRead()

Posted: 07 Mar 2019, 09:18
by megnatar
This function can replace the IniRead command native to Autohotkey.
It will read all the variables stored in a ini and makes them known to the calling script by creating them under fly.
Useful because it can save many lines in a script.

Might be useful to others...

Code: Select all

ReadIni(InputFile) {                                                ; InputFile should be full path or var containing path to the ini. ReadIni("z:\IniFile.ini")
    local TmpVar                                                    ; Set TmpVar local to the function. All variables created by references name
                                                                    ; will be global to the calling script.
    Loop, parse, % FileOpen(InputFile, 0).read(), `n, `r            ; Use FileOpen() and parse the fileObject directly in to the loop using read() method.
    {                                                               ; A_LoopField will contain the 'current' line in the ini file.
    
        if (((InStr(A_LoopField, "[")) = 1 ? 1) || ((InStr(A_LoopField, "`;")) = 1 ? 1) || !A_LoopField) ; Comments and Sections Should start on the first
            Continue                                                                                     ; position on each line found in the ini file!
            
        TmpVar := InStr(SubStr(A_LoopField, 1, InStr(A_LoopField, "=")-1), ";" ; Assign variable name to TmpVar. The name will be referenced below.
        %TmpVar% := SubStr(A_LoopField, InStr(A_LoopField, "=")+1)             ; Assign the value for the variable name, referenced in %TmpVar%.
    }                                                                          ;  If TmpVar contains string GuiX, then the script will read '%TmpVar% := ...'
}                                                                              ;  as it is written 'GuiX := ...'

; by Megnatar.

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>.*)$)",_)