classes/New-BlockFactory.ps1

function New-BlockFactory {
    #######################
    # BlockFactory Class #
    #######################
    # BlockFactory is a stateful factory that constructs Block Objects, a Configuration. It keeps a list of Blocks.
    $BlockFactory = [PSCustomObject]@{
        'Constants' = [scriptblock]{
            # Constants
            $g_globaloptions_allowed_str = 'compress,compresscmd,uncompresscmd,compressext,compressoptions,uncompressoptions,copy,copytruncate,create,daily,dateext,dateformat,delaycompress,extension,ifempty,mail,mailfirst,maillast,maxage,minsize,missingok,monthly,nocompress,nocopy,nocopytruncate,nocreate,nodelaycompress,nodateext,nomail,nomissing,noolddir,nosharedscripts,noshred,notifempty,olddir,rotate,size,sharedscripts,shred,shredcycle,start,tabooext,weekly,yearly'
            $g_options_not_singleline_str = 'postrotate,prerotate,firstaction,lastaction,preremove';
            $g_options_not_switches_str = 'compresscmd,uncompresscmd,compressext,compressoptions,uncompressoptions,create,dateformat,extension,include,mail,maxage,minsize,olddir,postrotate,prerotate,firstaction,lastaction,preremove,rotate,size,shredcycle,start,tabooext'

            # Constants as arrays
            [string[]]$g_globaloptions_allowed = $g_globaloptions_allowed_str.Split(',')
            [string[]]$g_options_not_singleline = $g_options_not_singleline_str.Split(',');
            [string[]]$g_localoptions_allowed = $g_globaloptions_allowed + $g_options_not_singleline
            [string[]]$g_options_not_switches = $g_options_not_switches_str.Split(',')

            # Define our config-capturing regexes
            [Regex]$g_localconfigs_regex = '([^\n]*)({(?:(?:(firstaction|lastaction|prerotate|postrotate|preremove)(?:\s|.)*?endscript)|[^}])*})'
            [Regex]$g_globaloptions_allowed_regex = "(?:^|\n)[^\S\n]*\b($( ($g_globaloptions_allowed -join '|') ))\b(.*)"
            [Regex]$g_localoptions_allowed_regex = "\n[^\S\n]*(?:\b($( ($g_globaloptions_allowed -join '|') ))\b(.*)|\b(postrotate|prerotate|firstaction|lastaction|preremove)[^\n]*\n((?:.|\s)*?)\n.*\b(endscript)\b)"
            [hashtable]$g_no_yes = @{
                'nocompress' = 'compress'
                'nocopy' = 'copy'
                'nocopytruncate' = 'copytruncate'
                'nocreate' = 'create'
                'nodelaycompress' = 'delaycompress'
                'nodateext' = 'dateext'
                'nomail' = 'mail'
                'nomissingok' = 'missingok'
                'notifempty' = 'ifempty'
                'noolddir' = 'olddir'
                'nosharedscripts' = 'sharedscripts'
                'noshred' = 'shred'
            }
            #[Regex]$globalconfig_regex = '<?!#(' + ($g_globaloptions_allowed -join '|') + ')'
        }
        'GlobalOptions' = @{
            'compresscmd' = "C:\Program Files\7-Zip\7z.exe"
            'uncompresscmd' = "C:\Program Files\7-Zip\7z.exe"
            'compressext' = '.7z'
            'compressoptions' = 'a -t7z'
            'uncompressoptions' = 'x -t7z'
            'size' = ''
            'dateformat' = '-%Y%m%d'
            'nomissingok' = $true
            'rotate' = 4
            'start' = 1
            'tabooext' = '.rpmorig, .rpmsave, .swp, .rpmnew, ~, .cfsaved, .rhn-cfg-tmp-*.'

            'force' = $force
        }
        'Blocks' = [ordered]@{}
        'UniqueLogFileNames' = New-Object System.Collections.ArrayList
        'PrivateMethods' = [scriptblock]{
            function Get-Options {
                param (
                    [string]$configString,
                    [hashtable]$options_found,
                    [string[]]$options_allowed,
                    [Regex]$options_allowed_regex,
                    [string[]]$options_not_switches
                )

                $matches = $options_allowed_regex.Matches($configString)
                if ($matches.success) {
                    $matches | ForEach-Object {
                        # Get key and value
                        $match = $_
                        $key = if ($match.Groups[1].Value) { $match.Groups[1].Value } else { $match.Groups[3].Value }
                        $value = if ($match.Groups[2].Value) { $match.Groups[2].Value } else { $match.Groups[4].Value }

                        #Write-Verbose "`nLine: $line"
                        #Write-Verbose "key: $key"
                        #Write-Verbose "value: $value"
                        #Write-Verbose "Contains: $($options_allowed.Contains($key))"

                        # Store this option to hashtable. If there are duplicate options, later override earlier ones.
                        if ($key) {
                            if ($options_allowed.Contains($key)) {
                                $options_found[$key] = if ($options_not_switches.Contains($key)) {
                                                            # Don't trim if it's an option with a multiline value
                                                            if (!$g_options_not_singleline.Contains($key)) {
                                                                $value.Trim()
                                                            }else {
                                                                $value
                                                            }
                                                        } else { $true }
                            }
                        }
                    }
                }
                #$options_found
            }

            function Override-Options {
                param (
                    [hashtable]$child,
                    [hashtable]$parent
                )
                # Override my parent options with my options
                $my_options = $parent.Clone()
                $child.GetEnumerator() | ForEach-Object {
                    $key = $_.Name
                    $value = $_.Value
                    $my_options[$key] = $value
                }

                # When I said yes, and I didn't say no, but my parent said no, I will still go ahead.
                $g_no_yes.GetEnumerator() | ForEach-Object {
                    $no = $_.Name
                    $yes = $_.Value

                    if ( $child.ContainsKey($yes) -and (!$child.ContainsKey($no)) -and $parent.ContainsKey($no) ) {
                        $my_options.Remove($no)
                    }
                }
                $my_options
            }

            # Returns an array of log files, that match a given blockpath pattern but whose fullpath is not already present in a unique store
            function Get-Block-Logs {
                param ([object]$blockObject)

                $blockpath = $blockObject['Path']
                $opt_tabooext = $blockObject['Options']['tabooext']
                $opt_missingok = if ($blockObject['Options']['notmissingok']) { $false } else { $blockObject['Options']['missingok'] }

                # Split the blockpath pattern by spaces, to get either 1) log paths or 2) wildcarded-paths
                $logpaths = [System.Collections.Arraylist]@()
                $matches = [Regex]::Matches($blockpath, '"([^"]+)"|([^\s]+)')
                if ($matches.success) {
                    $matches | ForEach-Object {
                        $path = if ($_.Groups[1].Value.Trim()) {
                                    $_.Groups[1].Value
                                }else {
                                    $_.Groups[2].Value
                                }
                        if ($path) {
                            $logpaths.Add($path) | Out-Null
                        }
                    }
                }

                # Get all the log files matching path patterns defined in the Config.
                $logfiles = New-Object System.Collections.Arraylist
                foreach ($logpath in $logpaths) {
                    # Test if the path (without any wildcards) exists
                    if ($logpath -match '\*') {

                        # It's a wildcarded path.
                        Write-Verbose "Considering wildcarded path $logpath"

                        if (Test-Path -Path $logpath) {
                            # Add all files that match the wildcard path
                            $items = Get-ChildItem $logpath -File | Where-Object { (likeIn $_.Name $opt_tabooext.Split(',').Trim()) -eq $false }
                            $items | ForEach-Object {
                                $logfiles.Add($_) | Out-Null
                            }
                        }else {
                            if (!$opt_missingok) {
                                Write-Verbose "Excluding wildcarded path $logpath for rotation, because it doesn't exist!"
                            }
                        }
                    }else {
                        # It's a non-wildcarded path. Reject if it's a folder.

                        if (Test-Path -LiteralPath $logpath) {
                            $item = Get-Item -Path $logpath
                            if (Test-Path $item.FullName -PathType Container) {
                                # It's a directory. Ignore it.
                                if (!$opt_missingok) {
                                    Write-Verbose "Excluding path $logpath for rotation, because it is a directory. Directories cannot be rotated. If rotating all the files in the directory, append a wildcard to the end of the path."
                                }
                            }else {
                                # It's a file. Add
                                $logfiles.Add($item) | Out-Null
                            }
                        }else {
                            if (!$opt_missingok) {
                                Write-Verbose "Excluding log $logpath for rotation, because it doesn't exist or does not point to file!"
                            }
                        }
                    }
                }

                # Add unique log files to our list. If a logs already added, it must be a duplicate so we ignore it.
                if ($logfiles.Count) {
                    $logfileCount = $logfiles.Count - 1
                    foreach ($i in (0..$logfileCount)) {
                        $logfile = $logfiles[$i]
                        if ($logfile.FullName -in $this.UniqueLogFileNames) {
                            Write-Verbose "CONFIG: WARNING - Duplicate Log included: $($logfile.FullName) (matched in block pattern: $blockpath). Skipping rotation for this entry."
                            $logfiles.Remove($logfile)
                        }else {
                            $this.UniqueLogFileNames.Add($logfile.FullName) | Out-Null
                        }
                    }
                }

                $logfiles
            }

        }
    }
    $BlockFactory | Add-Member -Name 'Create' -MemberType ScriptMethod -Value {
        param ([string]$FullConfig)

        # Unpack my properties
        . $this.Constants

        # Unpack my methods
        . $this.PrivateMethods

        # Parse Full Config for global options as hashtable
        $globalconfig = $g_localconfigs_regex.Replace($FullConfig, '')
        Get-Options $globalconfig $this.GlobalOptions $g_globaloptions_allowed $g_globaloptions_allowed_regex $g_options_not_switches

        # Parse Full Config for all found local block(s) path pattern, options, and matching log files, storing them as hashtable. Override the global options.
        # TODO: Regex for localconfigs to match paths on multiple lines before { }
        $matches = $g_localconfigs_regex.Matches($FullConfig)
        if ($matches.success) {
            foreach ($localconfig in $matches) {
                # NOTE: NOT USED ANYMORE: A block pattern should delimit multiple paths with a single space
                #$my_path_pattern = ($localconfig.Groups[1].Value -Split ' ' | Where-Object { $_.Trim() }).Trim() -join ' '
                # Just get the raw path pattern
                $my_path_pattern = $localconfig.Groups[1].Value.Trim()
                if ($my_path_pattern -in $this.Blocks.Keys) {
                    Write-Verbose "CONFIG: WARNING - Duplicate path pattern $my_path_pattern . Only the latest entry will be used."
                }
                # Any duplicate block path pattern overrides the previous
                $this.Blocks[$my_path_pattern] = @{
                    'Path' = $my_path_pattern
                    'Options' = @{}
                    'LocalOptions' = @{}
                    'LogFiles' = ''
                }
                try {
                    Get-Options $localconfig.Groups[2].Value $this.Blocks[$my_path_pattern]['LocalOptions'] $g_localoptions_allowed $g_localoptions_allowed_regex $g_options_not_switches
                    $this.Blocks[$my_path_pattern]['Options'] = Override-Options $this.Blocks[$my_path_pattern]['LocalOptions'] $this.GlobalOptions
                    $this.Blocks[$my_path_pattern]['LogFiles'] = Get-Block-Logs $this.Blocks[$my_path_pattern] $this.UniqueLogFileNames
                } catch {
                    throw
                }
            }
        }else {
            Write-Verbose "CONFIG: WARNING - No configuration blocks were found."
        }
    }
    $BlockFactory | Add-Member -Name 'GetAll' -MemberType ScriptMethod -Value {
        $this.Blocks
    }

    $BlockFactory
}