Private/Read-Properties.ps1

function Read-Properties {
    <#
        .SYNOPSIS
        Parse properties file

        .DESCRIPTION
        Parse properties file to generate configuration variables

        .PARAMETER Path
        [String] The patch parameter corresponds to the path to the property file to read.

        .PARAMETER Section
        [Switch] The Section parameter indicates if properties should be grouped depending on existing sections in the file.

        .PARAMETER Metadata
        [Switch] The metadata parameter indicates that the value (data) as well as the description and section (metadata) should be returned. This does not apply is the section switch is enabled.

        .OUTPUTS
        [System.Collections.Specialized.OrderedDictionary] Read-Properties returns an ordered hash table containing the content of the property file.

        .EXAMPLE
        Read-Properties -Path ".\conf\default.ini" -Section

        In this example, Read-Properties will parse the default.ini file contained in the .\conf directory and generate an ordered hashtable containing the key-values pairs.

        .NOTES
        File name: Read-Properties.ps1
        Author: Florian Carrier
        Creation date: 2018-11-27
        Last modified: 2024-09-13
    #>

    [CmdletBinding ()]
    Param (
        [Parameter (
            Position    = 1,
            Mandatory   = $true,
            HelpMessage = "Path to the property file"
        )]
        [ValidateNotNullOrEmpty ()]
        [String]
        $Path,
        [Parameter (
            Position    = 3,
            Mandatory   = $false,
            HelpMessage = "Define if section headers should be used to group properties or be ignored"
            )]
        [Switch]
        $Section,
        [Parameter (
            HelpMessage = "Switch to retrieve metadata about properties"
            )]
        [Switch]
        $Metadata
    )
    Begin {
        # Get global preference variables
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
    }
    Process {
        # Check that the file exists
        if (Test-Path -Path $Path) {
            # Load property file content
            $Content        = Get-Content -Path $Path
            # Instantiate variables
            $Properties     = New-Object -TypeName "System.Collections.Specialized.OrderedDictionary"
            $Sections       = New-Object -TypeName "System.Collections.Specialized.OrderedDictionary"
            $Errors         = 0
            $LineNumber     = 0
            $Header         = $null
            $PreviousLine   = $null
            # Read content line by line
            foreach ($Line in $Content) {
                $LineNumber += 1
                # If properties have to be grouped by section
                if ($Section) {
                    # If end of file and section is open
                    if ($LineNumber -eq $Content.Count -And $Header) {
                        if ($Line[0] -ne "#" -And $Line[0] -ne ";" -And $Line -ne "") {
                            $Property = Read-Property -Property $Line
                            if ($Property.Count -gt 0) {
                                $Sections.Add($Property.Key, $Property.Value)
                            } else {
                                Write-Log -Type "WARN" -Message "Unable to process line $LineNumber from $Path"
                            }
                        }
                        $Clone = Copy-OrderedHashtable -Hashtable $Sections -Deep
                        $Properties.Add($Header, $Clone)
                    } elseif ($Line[0] -eq "[") {
                        # If previous section exists add it to the property list
                        if ($Header) {
                            $Clone = Copy-OrderedHashtable -Hashtable $Sections -Deep
                            $Properties.Add($Header, $Clone)
                        }
                        # Create new property group
                        $Header = $Line.Substring(1, $Line.Length - 2)
                        $Sections.Clear()
                    } elseif ($Header -And $Line[0] -ne "#" -And $Line[0] -ne ";" -And $Line -ne "") {
                        $Property = Read-Property -Property $Line
                        if ($Property.Count -gt 0) {
                            $Sections.Add($Property.Key, $Property.Value)
                        } else {
                            Write-Log -Type "WARN" -Message "Unable to process line $LineNumber from $Path"
                        }
                    }
                } else {
                    # Parse rows
                    if ($null -eq $Line -or $Line -eq "") {
                        # Ignore empty lines
                    } elseif ($Line[0] -eq "[") {
                        # Parse sections
                        $Header = $Line.Substring(1, $Line.Length - 2).Trim()
                    } elseif ($Line[0] -eq "#" -Or $Line[0] -eq ";" ) {
                        # Parse comments
                        $PreviousLine = $Line.Substring(1, $Line.Length - 1).Trim()
                    } else {
                        # Parse properties
                        $Property = Read-Property -Property $Line
                        if ($Property.Count -gt 0) {
                            if ($Metadata -eq $true) {
                                # Create custom object including metadata
                                $Value = [Ordered]@{
                                    "Value"                 = $Property.Value
                                    "Description"     = $PreviousLine
                                    "Section"             = $Header
                                }
                            } else {
                                # Return raw value
                                $Value = $Property.Value
                            }
                            try {
                                # Assign property
                                $Properties.Add($Property.Key, $Value)
                            } catch {
                                Write-Log -Type "WARN" -Object "Two distinct definitions of the property $($Property.Key) have been found in the configuration file"
                                $Errors += 1
                            }
                            # Reset metadata
                            $PreviousLine = $null
                        } else {
                            Write-Log -Type "WARN" -Message "Unable to process line $LineNumber from $Path"
                        }
                    }
                }
            }
        } else {
            # Alert that configuration file does not exist at specified location
            Write-Log -Type "ERROR" -Message "Path not found $Path" -ExitCode 1
        }
        if ($Errors -gt 0) {
            Write-Log -Type "ERROR" -Object "Unable to proceed. Resolve the issues in $Path" -ExitCode 1
        }
    }
    End {
        # Return list of properties
        return $Properties
    }
}