Indented.IniFile.psm1
using namespace System.Collections.Generic using namespace System.IO using namespace System.Management.Automation using namespace System.Text enum Ensure { Absent Present } [DscResource()] class IniFileItem { [DscProperty()] [Ensure]$Ensure = 'Present' [DscProperty(Key)] [String]$Name [DscProperty()] [String]$Section [DscProperty()] [String]$Value [DscProperty()] [String]$NewValue [DscProperty(Key)] [String]$Path Hidden [Void] InitializeRequest() { if (-not (Test-Path $this.Path)) { throw 'The INI file, {0}, does not exist' -f $this.Path } } Hidden [Hashtable] GetParams() { $params = @{ Name = $this.Name Path = $this.Path } if ($this.Section) { $params.Add('Section', $this.Section) } if ($this.Value) { $params.Add('Value', $this.Value) } return $params } [IniFileItem] Get() { $this.InitializeRequest() $params = $this.GetParams() $item = Get-IniFileItem @params if ($item) { $this.Ensure = 'Present' $this.NewValue = $item.Value } else { $this.Ensure = 'Absent' } return $this } [Void] Set() { $this.InitializeRequest() $params = $this.GetParams() if ($this.Ensure -eq 'Present') { Set-IniFileItem @params -NewValue $this.NewValue } elseif ($this.Ensure -eq 'Absent') { Remove-IniFileItem @params } } [Boolean] Test() { $this.InitializeRequest() $params = $this.GetParams() $item = Get-IniFileItem @params if ($this.Ensure -eq 'Present') { if (-not $item) { return $false } if ($item -and $item.Value -ne $this.NewValue) { return $false } } elseif ($this.Ensure -eq 'Absent') { if ($item) { return $false } } return $true } } function GetEol { <# .SYNOPSIS Attempt to find the end of line character. .DESCRIPTION Used to find the end of line character in a file. #> [CmdletBinding()] [OutputType([System.String])] param ( # The path to an ini file. [Parameter(Mandatory)] [String]$Path ) try { $eol = [Environment]::NewLine $Path = $pscmdlet.GetUnresolvedProviderPathFromPSPath($Path) if (-not (Test-Path $Path)) { return $eol } $streamReader = [StreamReader][File]::OpenRead($Path) [Char[]]$buffer = [Char[]]::new(100) while (-not $streamReader.EndOfStream) { $null = $streamReader.Read($buffer, 0, 100) $newLineIndex = $buffer.IndexOf([Char]"`n") if ($newLineIndex -gt -1) { if ($buffer[$newLineIndex - 1] -eq [Char]"`r") { $streamReader.Close() return "`r`n" } else { $streamReader.Close() return "`n" } } } $streamReader.Close() return $eol } catch { $pscmdlet.ThrowTerminatingError($_) } } function UpdateString { [CmdletBinding()] param ( [Parameter(Mandatory)] [AllowEmptyString()] [String]$String, [Parameter(Mandatory)] [String]$Value, [Int32]$Position = $String.Length, [String]$EndOfLine = [Environment]::NewLine ) if ($Position -gt 0) { if ($String[$Position - 1] -ne "`n") { $Value = '{0}{1}' -f $EndOfLine, $Value } } if ($String.Length -gt $Position) { $Value = '{0}{1}' -f $Value, $EndOfLine } $String.Insert($Position, $Value) } function Get-IniFileItem { <# .SYNOPSIS Get an item from an Ini file. .DESCRIPTION Reads an Ini file, returning matching items. The ini file items returned by this function include an Extent property which describes the location of an item within the file. .EXAMPLE Get-IniFileItem -Path somefile.ini Get all items in somefile.ini. .EXAMPLE Get-IniFileItem -Section somesection -Path somefile.ini Get all items within the second "somesection" from somefile.ini. .EXAMPLE Get-IniFileItem -Name somename -Path somefile.ini Get all items named somename from any section. #> [CmdletBinding(DefaultParameterSetName = 'DefaultSearch')] [OutputType([System.Management.Automation.PSObject])] param ( # The name of the field to retrieve. The Name parameter supports wildcards. By default, all fields are returned. [Parameter(Position = 1)] [String]$Name = '*', # The section a setting resides within. The Section parameter supports wildcrds. By default, all sections are returned. [Parameter(Position = 2)] [string]$Section = '*', # The path to an ini file. [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [Alias('FullName')] [String]$Path, # The value may be defined to describe the item. Wildcards are supported. [Parameter(Mandatory, ParameterSetName = 'SearchUsingValue')] [String]$Value, # The literal value may be defined to absolutely describe the item. Wildcards are not supported. [Parameter(Mandatory, ParameterSetName = 'SearchUsingLiteralValue')] [String]$LiteralValue ) process { try { $Path = $pscmdlet.GetUnresolvedProviderPathFromPSPath($Path) $eolLength = (GetEol -Path $Path).Length $streamReader = [StreamReader][File]::OpenRead($Path) $position = 0 while (-not $streamReader.EndOfStream) { switch -Regex ($streamReader.ReadLine()) { '' { $position += $_.Length } '^;' { break } '^\[([^\]]+)\] *$' { $SectionName = $matches[1] $SectionStart = $position - $_.Length break } '^([^=]+?) *= *(.*)$' { [PSCustomObject]@{ Name = $matches[1] Value = $matches[2] Section = $SectionName Extent = [PSCustomObject]@{ SectionStart = $SectionStart ItemStart = $position - $_.Length ItemEnd = $position ItemLength = $_.Length ValueStart = $position - $matches[2].Length ValueLength = $matches[2].Length } Path = $Path PSTypeName = 'IniFileItem' } | Where-Object { $_.Name -like $Name -and $_.Section -like $Section -and ($pscmdlet.ParameterSetName -ne 'SearchUsingValue' -or $_.Value -like $Value) -and ($pscmdlet.ParameterSetName -ne 'SearchUsingLiteralValue' -or $_.Value -like $LiteralValue) } break } } # End of line character $position += $eolLength } } catch { $pscmdlet.WriteError($_) } finally { if ($streamReader) { $streamReader.Close() } } } } function Get-IniFileSection { <# .SYNOPSIS Get a section from an Ini file. .DESCRIPTION Reads an Ini file, returning matching sections. The ini file sections returned by this function includes an Extent property which describes the location of a section within the file. .EXAMPLE Get-IniFileItem -Path somefile.ini Get all sections in somefile.ini. .EXAMPLE Get-IniFileSection -Name somesection -Path somefile.ini Get the section named "somesection" from somefile.ini. #> [CmdletBinding()] [OutputType([System.Management.Automation.PSObject])] param ( # The name of the field to retrieve. The Name parameter supports wildcards. By default, all fields are returned. [String]$Name = '*', # The path to an ini file. [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [Alias('FullName')] [String]$Path ) process { try { $Path = $pscmdlet.GetUnresolvedProviderPathFromPSPath($Path) $eolLength = (GetEol -Path $Path).Length $streamReader = [StreamReader][File]::OpenRead($Path) $Section = [PSCustomObject]@{ Name = '<Undefined>' Items = @() Extent = [PSCustomObject]@{ Start = 0 End = 0 } } $position = 0 while (-not $streamReader.EndOfStream) { switch -Regex ($streamReader.ReadLine()) { '' { $position += $_.Length } '^;' { break } '^\[([^\]]+)\] *$' { if ($Section.Name -ne '<Undefined>' -or $Section.Items.Count -gt 0) { $Section.Extent.End = $lastPosition $Section | Where-Object Name -like $Name } $Section = [PSCustomObject]@{ Name = $matches[1] Items = @() Extent = [PSCustomObject]@{ Start = $position End = 0 } PSTypeName = 'IniFileSection' } break } '^([^=]+?) *= *(.*)$' { $Section.Items += [PSCustomObject]@{ Name = $matches[1] Value = $matches[2] } break } } $lastPosition = $position $position += $eolLength } $streamReader.Close() $Section.Extent.End = $lastPosition $Section | Where-Object Name -like $Name } catch { $pscmdlet.WriteError($_) } finally { if ($streamReader) { $streamReader.Close() } } } } function Remove-IniFileItem { <# .SYNOPSIS Get an item from an Ini file. .DESCRIPTION Reads an Ini file, returning matching items. .EXAMPLE Remove-IniFileItem -Name somename -Section somesection -Path somefile.ini Remove somename from the somesection section in somefile.ini. .EXAMPLE Remove-IniFileItem -Name somename -Path somefile.ini Remove somename from all sections in somefile.ini. .EXAMPLE Remove-IniFileItem -Name extension -Value imap -Section PHP Remove extension, when the value is imap, from the section PHP. #> [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'DefaultSearch')] [OutputType([Void])] param ( [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'FromPipeline')] [PSTypeName('IniFileItem')] [PSObject]$InputObject, # The name of the field to retrieve. [Parameter(Mandatory, Position = 1, ParameterSetName = 'DefaultSearch')] [Parameter(Mandatory, Position = 1, ParameterSetName = 'SearchUsingValue')] [Parameter(Mandatory, Position = 1, ParameterSetName = 'SearchUsingLiteralValue')] [String]$Name, # The section a setting resides within. If this value is not set the value will be removed from all sections. [Parameter(Position = 2, ParameterSetName = 'DefaultSearch')] [Parameter(Position = 2, ParameterSetName = 'SearchUsingValue')] [Parameter(Position = 2, ParameterSetName = 'SearchUsingLiteralValue')] [String]$Section, # The path to an ini file. [Parameter(Mandatory, Position = 3, ParameterSetName = 'DefaultSearch')] [Parameter(Mandatory, Position = 3, ParameterSetName = 'SearchUsingValue')] [Parameter(Mandatory, Position = 3, ParameterSetName = 'SearchUsingLiteralValue')] [ValidateScript( { Test-Path $_ -PathType Leaf } )] [String]$Path, # The value may be defined to describe the item. Wildcards are supported. [Parameter(Mandatory, ParameterSetName = 'SearchUsingValue')] [String]$Value, # The literal value may be defined to absolutely describe the item. Wildcards are not supported. [Parameter(Mandatory, ParameterSetName = 'SearchUsingLiteralValue')] [String]$LiteralValue ) begin { $null = $psboundparameters.Remove('WhatIf') if ($pscmdlet.ParameterSetName -eq 'FromPipeline') { $iniFileItems = [List[PSObject]]::new() } else { Get-IniFileItem @psboundparameters | Remove-IniFileItem } } process { if ($pscmdlet.ParameterSetName -eq 'FromPipeline') { $iniFileItems.Add($InputObject) } } end { if ($pscmdlet.ParameterSetName -eq 'FromPipeline' -and $iniFileItems.Count -gt 0) { foreach ($group in $iniFileItems | Group-Object Path) { $iniFilePath = $pscmdlet.GetUnresolvedProviderPathFromPSPath($group.Name) $eolLength = (GetEol -Path $iniFilePath).Length $streamReader = [StreamReader][File]::OpenRead($iniFilePath) $encoding = $streamReader.CurrentEncoding $content = $streamReader.ReadToEnd() $streamReader.Close() $items = $group.Group | Sort-Object { $_.Extent.ItemStart } -Descending foreach ($item in $items) { if ($item.Value -ne $Value) { if ($item.Length -gt $item.Extent.ItemStart + $item.Extent.ItemLength + $eolLength) { $itemLength = $item.Extent.ItemLength + $eolLength } else { $itemLength = $item.Extent.ItemLength } $content = $content.Remove($item.Extent.ItemStart, $itemLength) } } if ($pscmdlet.ShouldProcess(('Updating INI file {0}' -f $iniFilePath))) { [File]::WriteAllLines( $iniFilePath, $content, $encoding ) } } } } } function Set-IniFileItem { <# .SYNOPSIS Set the value of an item in an INI file. .DESCRIPTION Set the value of an item in an INI file. Set-IniFileItem allows .EXAMPLE Set-IniFileItem -Name itemName -NewValue someValue -Path config.ini Set a value for itemName in config.ini. .EXAMPLE Set-IniFileItem -Name itemName -Value currentValue -NewValue newValue -Path config.ini Set a new value for itemName with value currentValue. .EXAMPLE Set-IniFileItem -Name extension -Value ldap -NewValue ldap -Path php.ini Set a value for extension to LDAP. Other extension items in the file are ignored because of the Value filter. #> [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'DefaultSearch')] param ( # The name of an item to add or edit. # # If the item does not exist it will be created. Section is mandatory for new items. [Parameter(Mandatory, ValueFromPipelineByPropertyName, Position = 1)] [String]$Name, # The name of a section to add the item to. If the section does not exist it will be created. # # Section must be defined when adding a new value. [Parameter(ValueFromPipelineByPropertyName)] [String]$Section, # The value may be defined to describe the item. Wildcards are supported. [Parameter(Mandatory, ParameterSetName = 'SearchUsingValue')] [String]$Value, # The literal value may be defined to absolutely describe the item. Wildcards are not supported. [Parameter(Mandatory, ParameterSetName = 'SearchUsingLiteralValue')] [String]$LiteralValue, # The value of the item. [Parameter(Mandatory, Position = 2)] [String]$NewValue, # The path to an ini file. [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [Alias('FullName')] [String]$Path, # Request expansion of environment variables within the value. [Switch]$ExpandEnvironmentVariables, # Add spaces around the = symbol. For example, sets "Name = Value" instead of "Name=Value". [Switch]$IncludePadding, # If the INI file already exists the files current encoding will be used. If not, an encoding may be specified using this parameter. By default, files are saved using UTF8 encoding with no BOM. [Encoding]$Encoding = [System.Text.UTF8Encoding]::new($false), # The default end of line character used when creating new files. [ValidateSet("`r`n", "`n")] [String]$EndOfLine = [Environment]::NewLine ) process { $Path = $pscmdlet.GetUnresolvedProviderPathFromPSPath($Path) $eolParam = @{ EndOfLine = $EndOfLine } if ($ExpandEnvironmentVariables) { $null = $psboundparameters.Remove('ExpandEnvironmentVariables') $NewValue = [Environment]::ExpandEnvironmentVariables($NewValue) } $newItem = '{0}{2}={2}{1}' -f $Name, $NewValue, @('', ' ')[$IncludePadding.ToBool()] if (Test-Path $Path) { $eolParam['EndOfLine'] = GetEol -Path $Path $streamReader = [StreamReader][File]::OpenRead($Path) $content = $streamReader.ReadToEnd() $encoding = $streamReader.CurrentEncoding $streamReader.Close() $null = $psboundparameters.Remove('NewValue') $null = $psboundparameters.Remove('ExpandEnvironmentVariables') $null = $psboundparameters.Remove('IncludePadding') $null = $psboundparameters.Remove('Encoding') $null = $psboundparameters.Remove('EndOfLine') [PSObject[]]$existingItems = Get-IniFileItem @psboundparameters if ($existingItems) { [Array]::Reverse($existingItems) foreach ($existingItem in $existingItems) { if ($existingItem.Value -ne $NewValue) { $content = $content.Remove($existingItem.Extent.ValueStart, $existingItem.Extent.ValueLength). Insert($existingItem.Extent.ValueStart, $NewValue) } } } else { if (-not $Section) { $Section = '<Undefined>' } $existingSection = Get-IniFileSection -Name $Section -Path $Path if ($existingSection) { $position = $existingSection.Extent.End } else { if ($Section -eq '<Undefined>') { $position = 0 } else { $content = UpdateString -String $content -Value ('[{0}]' -f $Section) @eolParam $position = $content.Length } } $content = UpdateString -String $content -Value $newItem -Position $position @eolParam } } else { $content = '' if ($Section) { $content = UpdateString -String $content -Value ('[{0}]' -f $Section) @eolParam } $content = UpdateString -String $content -Value $newItem @eolParam } if ($pscmdlet.ShouldProcess(('Updating file {0}, {1} with value {2}' -f $Path, $Name, $NewValue))) { [File]::WriteAllLines( $Path, $content, $encoding ) } } } |