LabViewConfigHelper.psm1

#Requires -Version 5.1

<#
.SYNOPSIS
    LabVIEW Configuration Management Module
 
.DESCRIPTION
    Provides comprehensive functions to manage LabVIEW configuration files (INI format).
    Supports reading, writing, and maintaining multi-section configuration files with proper error handling.
 
.NOTES
    File Name : LabViewConfigHelper.psm1
    Author : LabVIEW Helpers Team
    Prerequisite : PowerShell 5.1 or higher
    Copyright 2025 : LabVIEW Helpers
#>


function Set-ConfigValue {
    <#
    .SYNOPSIS
        Sets a configuration value in an INI-style configuration file.
 
    .DESCRIPTION
        Updates or creates a configuration key-value pair in the specified section
        of the configuration file.
 
    .PARAMETER ConfigPath
        Path to the configuration file.
 
    .PARAMETER Section
        Configuration section name.
 
    .PARAMETER Key
        Configuration key name.
 
    .PARAMETER Value
        Configuration value to set.
 
    .PARAMETER CreateIfNotExists
        Creates the configuration file if it doesn't exist.
 
    .EXAMPLE
        Set-ConfigValue -ConfigPath "C:\LabVIEW.ini" -Section "LabVIEW" -Key "server.tcp.enabled" -Value "True"
 
    .EXAMPLE
        Set-ConfigValue -ConfigPath "C:\config.ini" -Section "Settings" -Key "timeout" -Value "30" -CreateIfNotExists
 
    .NOTES
        Supports multi-section INI file format with proper section handling.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [string]$ConfigPath,
        
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [string]$Section,
        
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [string]$Key,
        
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [AllowEmptyString()]
        [string]$Value,
        
        [Parameter()]
        [switch]$CreateIfNotExists
    )
    
    begin {
        Write-Verbose "Starting Set-ConfigValue operation"
    }
    
    process {
        try {
            # Check if file exists
            if (-not (Test-Path -LiteralPath $ConfigPath)) {
                if ($CreateIfNotExists) {
                    $directory = Split-Path -Path $ConfigPath -Parent
                    if ($directory -and -not (Test-Path -LiteralPath $directory)) {
                        New-Item -Path $directory -ItemType Directory -Force | Out-Null
                    }
                    New-Item -Path $ConfigPath -ItemType File -Force | Out-Null
                    Write-Verbose "Created config file: $ConfigPath"
                } else {
                    throw "Config file not found: $ConfigPath"
                }
            }
    
            # Read the file content - handle both PowerShell versions
            $content = @(Get-Content -LiteralPath $ConfigPath -ErrorAction SilentlyContinue)
            if ($null -eq $content -or $content.Count -eq 0) { 
                $content = @() 
            }
            
            $sectionFound = $false
            $keyFound = $false
            $newContent = [System.Collections.ArrayList]::new()
            
            # Process each line
            for ($i = 0; $i -lt $content.Count; $i++) {
                $line = $content[$i]
                
                # Check if we found the section
                if ($line -match "^\[$([regex]::Escape($Section))\]$") {
                    $sectionFound = $true
                    $null = $newContent.Add($line)
                    continue
                }
                
                # Check if we're in the right section and found the key
                if ($sectionFound -and $line -match "^$([regex]::Escape($Key))=") {
                    $keyFound = $true
                    $null = $newContent.Add("$Key=$Value")
                    Write-Verbose "Updated: [$Section] $Key = $Value"
                    continue
                }
                
                # Check if we hit a new section (and we were in our target section)
                if ($sectionFound -and $line -match "^\[.+\]$" -and $line -notmatch "^\[$([regex]::Escape($Section))\]$") {
                    # We've moved to a new section, add the key if not found
                    if (-not $keyFound) {
                        $null = $newContent.Add("$Key=$Value")
                        Write-Verbose "Added: [$Section] $Key = $Value"
                        $keyFound = $true
                    }
                    $sectionFound = $false
                }
                
                $null = $newContent.Add($line)
            }
            
            # If section was found but key wasn't, add it at the end of the section
            if ($sectionFound -and -not $keyFound) {
                $null = $newContent.Add("$Key=$Value")
                Write-Verbose "Added: [$Section] $Key = $Value"
                $keyFound = $true
            }
            
            # If section wasn't found, add it with the key
            if (-not $sectionFound) {
                $null = $newContent.Add("")
                $null = $newContent.Add("[$Section]")
                $null = $newContent.Add("$Key=$Value")
                Write-Verbose "Added new section: [$Section] with $Key = $Value"
            }
            
            # Write back to file - ensure consistent encoding
            $newContent.ToArray() | Set-Content -LiteralPath $ConfigPath -Encoding UTF8
            Write-Verbose "Config file updated: $ConfigPath"
        }
        catch {
            Write-Error "Failed to set config value [$Section] $Key in $ConfigPath`: $_"
            throw
        }
    }
    
    end {
        Write-Verbose "Completed Set-ConfigValue operation"
    }
}

function Get-ConfigValue {
    <#
    .SYNOPSIS
        Retrieves a configuration value from an INI-style configuration file.
 
    .DESCRIPTION
        Reads a configuration key from the specified section of the configuration file.
 
    .PARAMETER ConfigPath
        Path to the configuration file.
 
    .PARAMETER Section
        Configuration section name.
 
    .PARAMETER Key
        Configuration key name.
 
    .EXAMPLE
        Get-ConfigValue -ConfigPath "C:\LabVIEW.ini" -Section "LabVIEW" -Key "server.tcp.enabled"
 
    .NOTES
        Returns $null if the key is not found.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [string]$ConfigPath,
        
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [string]$Section,
        
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [string]$Key
    )
    
    begin {
        Write-Verbose "Starting Get-ConfigValue operation"
    }
    
    process {
        try {
            if (-not (Test-Path -LiteralPath $ConfigPath)) {
                throw "Config file not found: $ConfigPath"
            }
            
            $content = @(Get-Content -LiteralPath $ConfigPath)
            $inSection = $false
            
            foreach ($line in $content) {
                # Check if we found the section
                if ($line -match "^\[$([regex]::Escape($Section))\]$") {
                    $inSection = $true
                    continue
                }
                
                # Check if we hit a new section
                if ($line -match "^\[.+\]$") {
                    $inSection = $false
                    continue
                }
                
                # If we're in the right section, look for the key
                if ($inSection -and $line -match "^$([regex]::Escape($Key))=(.*)$") {
                    Write-Verbose "Found value for [$Section] $Key"
                    return $matches[1]
                }
            }
            
            Write-Verbose "Key not found: [$Section] $Key"
            return $null
        }
        catch {
            Write-Error "Failed to get config value [$Section] $Key from $ConfigPath`: $_"
            throw
        }
    }
    
    end {
        Write-Verbose "Completed Get-ConfigValue operation"
    }
}

function Remove-ConfigValue {
    <#
    .SYNOPSIS
        Removes a configuration value from an INI-style configuration file.
 
    .DESCRIPTION
        Deletes a configuration key from the specified section of the configuration file.
 
    .PARAMETER ConfigPath
        Path to the configuration file.
 
    .PARAMETER Section
        Configuration section name.
 
    .PARAMETER Key
        Configuration key name to remove.
 
    .EXAMPLE
        Remove-ConfigValue -ConfigPath "C:\LabVIEW.ini" -Section "LabVIEW" -Key "server.tcp.enabled"
 
    .NOTES
        Displays a warning if the key is not found.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [string]$ConfigPath,
        
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [string]$Section,
        
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [string]$Key
    )
    
    begin {
        Write-Verbose "Starting Remove-ConfigValue operation"
    }
    
    process {
        try {
            if (-not (Test-Path -LiteralPath $ConfigPath)) {
                throw "Config file not found: $ConfigPath"
            }
            
            $content = @(Get-Content -LiteralPath $ConfigPath)
            $newContent = [System.Collections.ArrayList]::new()
            $inSection = $false
            $removed = $false
            
            foreach ($line in $content) {
                # Check if we found the section
                if ($line -match "^\[$([regex]::Escape($Section))\]$") {
                    $inSection = $true
                    $null = $newContent.Add($line)
                    continue
                }
                
                # Check if we hit a new section
                if ($line -match "^\[.+\]$") {
                    $inSection = $false
                    $null = $newContent.Add($line)
                    continue
                }
                
                # If we're in the right section and found the key, skip it
                if ($inSection -and $line -match "^$([regex]::Escape($Key))=") {
                    $removed = $true
                    Write-Verbose "Removed: [$Section] $Key"
                    continue
                }
                
                $null = $newContent.Add($line)
            }
            
            if ($removed) {
                $newContent.ToArray() | Set-Content -LiteralPath $ConfigPath -Encoding UTF8
                Write-Verbose "Config file updated: $ConfigPath"
            } else {
                Write-Warning "Key not found: [$Section] $Key"
            }
        }
        catch {
            Write-Error "Failed to remove config value [$Section] $Key from $ConfigPath`: $_"
            throw
        }
    }
    
    end {
        Write-Verbose "Completed Remove-ConfigValue operation"
    }
}

function Get-AllConfigValues {
    <#
    .SYNOPSIS
        Retrieves all configuration values from an INI-style configuration file.
 
    .DESCRIPTION
        Reads all configuration keys and values from the entire file or a specific section.
 
    .PARAMETER ConfigPath
        Path to the configuration file.
 
    .PARAMETER Section
        Optional. Configuration section name. If not specified, returns all sections.
 
    .EXAMPLE
        Get-AllConfigValues -ConfigPath "C:\LabVIEW.ini"
 
    .EXAMPLE
        Get-AllConfigValues -ConfigPath "C:\LabVIEW.ini" -Section "LabVIEW"
 
    .NOTES
        Returns a hashtable with sections as keys and nested hashtables for key-value pairs.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [string]$ConfigPath,
        
        [Parameter(ValueFromPipelineByPropertyName)]
        [string]$Section = $null
    )
    
    begin {
        Write-Verbose "Starting Get-AllConfigValues operation"
    }
    
    process {
        try {
            if (-not (Test-Path -LiteralPath $ConfigPath)) {
                throw "Config file not found: $ConfigPath"
            }
            
            $content = @(Get-Content -LiteralPath $ConfigPath)
            $result = @{}
            $currentSection = $null
            
            foreach ($line in $content) {
                # Check for section headers
                if ($line -match "^\[(.+)\]$") {
                    $currentSection = $matches[1]
                    continue
                }
                
                # Check for key-value pairs
                if ($line -match "^([^=]+)=(.*)$") {
                    $key = $matches[1]
                    $value = $matches[2]
                    
                    if ($Section -eq $null -or $currentSection -eq $Section) {
                        if (-not $result.ContainsKey($currentSection)) {
                            $result[$currentSection] = @{}
                        }
                        $result[$currentSection][$key] = $value
                    }
                }
            }
            
            Write-Verbose "Retrieved configuration values from $ConfigPath"
            return $result
        }
        catch {
            Write-Error "Failed to get all config values from $ConfigPath`: $_"
            throw
        }
    }
    
    end {
        Write-Verbose "Completed Get-AllConfigValues operation"
    }
}

function Show-ConfigFile {
    <#
    .SYNOPSIS
        Displays the contents of an INI-style configuration file.
 
    .DESCRIPTION
        Shows all configuration sections, keys, and values in a formatted output.
 
    .PARAMETER ConfigPath
        Path to the configuration file.
 
    .PARAMETER Section
        Optional. Configuration section name. If not specified, shows all sections.
 
    .EXAMPLE
        Show-ConfigFile -ConfigPath "C:\LabVIEW.ini"
 
    .EXAMPLE
        Show-ConfigFile -ConfigPath "C:\LabVIEW.ini" -Section "LabVIEW"
 
    .NOTES
        Displays configuration in a readable format with color coding.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [string]$ConfigPath,
        
        [Parameter(ValueFromPipelineByPropertyName)]
        [string]$Section = $null
    )
    
    begin {
        Write-Verbose "Starting Show-ConfigFile operation"
    }
    
    process {
        try {
            if (-not (Test-Path -LiteralPath $ConfigPath)) {
                throw "Config file not found: $ConfigPath"
            }
            
            $allValues = Get-AllConfigValues -ConfigPath $ConfigPath -Section $Section
            
            Write-Host "Configuration File: $ConfigPath" -ForegroundColor Yellow
            Write-Host ("-" * 60) -ForegroundColor Gray
            
            foreach ($sectionName in $allValues.Keys) {
                Write-Host "[$sectionName]" -ForegroundColor Cyan
                foreach ($key in $allValues[$sectionName].Keys) {
                    $value = $allValues[$sectionName][$key]
                    Write-Host " $key = $value" -ForegroundColor White
                }
                Write-Host ""
            }
            
            Write-Verbose "Displayed configuration from $ConfigPath"
        }
        catch {
            Write-Error "Failed to show config file $ConfigPath`: $_"
            throw
        }
    }
    
    end {
        Write-Verbose "Completed Show-ConfigFile operation"
    }
}

# Export all functions
Export-ModuleMember -Function Set-ConfigValue, Get-ConfigValue, Remove-ConfigValue, Get-AllConfigValues, Show-ConfigFile