Private/Out-IniFile.ps1

Function Out-IniFile {
    <#
        .SYNOPSIS
            Write hash content to INI file
 
        .DESCRIPTION
            Writes a hashtable's content to an INI file with support for:
            - Sections and key-value pairs
            - Comments
            - Multiple encodings
            - File append mode
            - Read-only file overwrite
 
        .PARAMETER Append
            Adds the output to the end of an existing file, instead of replacing the file contents.
 
        .PARAMETER Encoding
            Specifies the character encoding. Default is Unicode.
            Valid values: Unicode, UTF7, UTF8, UTF32, ASCII, BigEndianUnicode, Default, OEM
 
        .PARAMETER FilePath
            Path to the output INI file.
 
        .PARAMETER Force
            Allows overwriting read-only files.
 
        .PARAMETER InputObject
            Hashtable containing the INI content to write.
 
        .PARAMETER PassThru
            Returns the file object after writing.
 
        .INPUTS
            System.Collections.Hashtable
            You can pipe a hashtable to this function.
 
        .OUTPUTS
            System.IO.FileSystemInfo when using -PassThru
            System.Void otherwise
 
        .EXAMPLE
            $config = @{
                'Section1' = @{
                    'Key1' = 'Value1'
                    'Key2' = 'Value2'
                }
            }
            Out-IniFile -InputObject $config -FilePath 'C:\config.ini'
 
            Writes a hashtable with one section to an INI file.
 
        .EXAMPLE
            $config | Out-IniFile -FilePath 'C:\config.ini' -Force -Encoding UTF8
 
            Pipes a hashtable to the function and writes it with UTF8 encoding, overwriting any existing file.
 
        .NOTES
            Used Functions:
                Name ║ Module/Namespace
                ═══════════════════════════════════════╬══════════════════════════════
                Write-Verbose ║ Microsoft.PowerShell.Utility
                Write-Debug ║ Microsoft.PowerShell.Utility
                Write-Error ║ Microsoft.PowerShell.Utility
                New-Item ║ Microsoft.PowerShell.Management
                Get-Item ║ Microsoft.PowerShell.Management
                Add-Content ║ Microsoft.PowerShell.Management
                Get-FunctionDisplay ║ EguibarIT
                Sort-Object ║ Microsoft.PowerShell.Utility
 
        .NOTES
            Version: 2.1
            DateModified: 22/May/2025
            LastModifiedBy: Vicente Rodriguez Eguibar
                            vicente@eguibar.com
                            Eguibar IT
                            http://www.eguibarit.com
 
            Based on work by: Oliver Lipkau <oliver@lipkau.net>
            Source: https://github.com/lipkau/PsIni
 
        .LINK
            https://github.com/vreguibar/EguibarIT/blob/main/Private/Out-IniFile.ps1
 
        .COMPONENT
            Configuration Management
 
        .ROLE
            File Processing
 
        .FUNCTIONALITY
            INI File Generation
    #>


    [CmdletBinding(
        SupportsShouldProcess = $true,
        ConfirmImpact = 'Low'
    )]
    [OutputType([System.IO.FileSystemInfo], ParameterSetName = 'PassThru')]
    [OutputType([void])]

    Param(
        [Parameter(Position = 0)]
        [switch]
        $Append,

        [Parameter(Position = 1)]
        [ValidateSet('Unicode', 'UTF7', 'UTF8', 'UTF32', 'ASCII', 'BigEndianUnicode', 'Default', 'OEM', ignorecase = $false)]
        [PSDefaultValue(Help = 'Default Value is "Unicode"')]
        [string]
        $Encoding = 'Unicode',

        [Parameter(Mandatory = $true,
            Position = 2,
            HelpMessage = 'Path to the output INI file')]
        [ValidateNotNullOrEmpty()]
        [Alias('Path', 'File')]
        [string]
        $FilePath,

        [Parameter(Mandatory = $false,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            HelpMessage = 'If present, the function will not ask for confirmation when performing actions.',
            Position = 3)]
        [Switch]
        $Force,

        [Parameter(Mandatory = $true,
            ValueFromPipeline = $true,
            HelpMessage = 'Hashtable containing INI content')]
        [ValidateNotNull()]
        [Alias('Hash', 'Content')]
        [hashtable]
        $InputObject,

        [Parameter(Position = 4)]
        [switch]
        $Passthru
    )

    Begin {
        Set-StrictMode -Version Latest

        # Output header information
        if ($null -ne $Variables -and
            $null -ne $Variables.Header) {

            $txt = ($Variables.Header -f
                (Get-Date).ToString('dd/MMM/yyyy'),
                $MyInvocation.Mycommand,
                (Get-FunctionDisplay -HashTable $PsBoundParameters -Verbose:$False)
            )
            Write-Verbose -Message $txt
        } #end If

        ##############################
        # Variables Definition

        # StringBuilder for better performance
        $sb = [System.Text.StringBuilder]::new()

    } #end Begin

    Process {

        try {
            # Create or get the file
            if ($PSBoundParameters['Append']) {

                $outFile = Get-Item -Path $FilePath -ErrorAction Stop
                Write-Debug -Message ('Appending to existing file: {0}' -f $FilePath)

            } else {

                if ($PSCmdlet.ShouldProcess($FilePath, 'Create new INI file')) {

                    $outFile = New-Item -ItemType File -Path $FilePath -Force:$Force -ErrorAction Stop
                    Write-Debug -Message ('Created new file: {0}' -f $FilePath)

                } #End If

            } #end If-Else

            if (-not $outFile) {
                Throw 'Could not create File'
            } #end If

            # Process each key in the hashtable
            foreach ($section in $InputObject.Keys) {

                Write-Debug -Message ('Processing section: {0}' -f $section)

                if ($InputObject[$section] -isnot [hashtable]) {

                    # Direct key-value pair
                    [void]$sb.AppendLine('{0}={1}' -f $section, $InputObject[$section])
                    Write-Debug -Message ('Writing key: {0}' -f $section)

                } else {

                    # Section with nested keys
                    [void]$sb.AppendLine('[{0}]' -f $section)
                    Write-Debug -Message ('Writing section: [{0}]' -f $section)

                    foreach ($key in ($InputObject[$section].Keys | Sort-Object)) {

                        if ($key -match '^Comment[\d]+') {

                            [void]$sb.AppendLine($InputObject[$section][$key])
                            Write-Debug -Message ('Writing comment: {0}' -f $InputObject[$section][$key])

                        } else {

                            [void]$sb.AppendLine('{0}={1}' -f $key, $InputObject[$section][$key])
                            Write-Debug -Message ('Writing key: {0}' -f $key)
                        } #end If-Else

                    } #end Foreach

                    [void]$sb.AppendLine()
                } #end If-Else
            } #end Foreach

            # Write content to file
            if ($PSCmdlet.ShouldProcess($FilePath, 'Write content')) {

                Add-Content -Path $outFile -Value $sb.ToString() -Encoding $Encoding -ErrorAction Stop

            } #end If

            if ($PSBoundParameters['Passthru']) {
                Return $outfile
            } #end If

        } catch {

            Write-Error -Message ('Failed to write INI file: {0}' -f $_.Exception.Message)
            throw

        } #end Try-Catch

    } #end Process

    End {

        if ($null -ne $Variables -and
            $null -ne $Variables.Footer) {

            $txt = ($Variables.Footer -f $MyInvocation.InvocationName,
                'writing to INI file (Private Function).'
            )
            Write-Verbose -Message $txt
        } #end If

    } #end End

} #end Function Out-IniFile