PSPublishModule.psm1

function Add-Directory {
    [CmdletBinding()]
    param($dir)
    $exists = Test-Path -Path $dir
    if ($exists -eq $false) { $null = mkdir $dir }
}
function Add-FilesWithFolders {
    [CmdletBinding()]
    param ($file, $FullProjectPath, $directory)
    $LinkPrivatePublicFiles = foreach ($dir in $directory) { if ($file -like "$dir*") { $file } }
    $LinkPrivatePublicFiles
}
function Add-ObjectTo {
    [CmdletBinding()]
    param($Object, $Type)
    Write-Verbose "Adding $($Object) to $Type"
    return $Object
}
function Copy-File {
    [CmdletBinding()]
    param ($Source,
        $Destination)
    if ((Test-Path $Source) -and !(Test-Path $Destination)) { Copy-Item -Path $Source -Destination $Destination }
}
function Export-PSData {
    [cmdletbinding()]
    <#
    .Synopsis
        Exports property bags into a data file
    .Description
        Exports property bags and the first level of any other object into a ps data file (.psd1)
    .Link
        https://github.com/StartAutomating/Pipeworks
        Import-PSData
    .Example
        Get-Web -Url http://www.youtube.com/watch?v=xPRC3EDR_GU -AsMicrodata -ItemType http://schema.org/VideoObject |
            Export-PSData .\PipeworksQuickstart.video.psd1
    #>

    [OutputType([IO.FileInfo])]
    param([Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [PSObject[]]
        $InputObject,
        [Parameter(Mandatory = $true, Position = 0)]
        [string]
        $DataFile)
    begin { $AllObjects = New-Object Collections.ArrayList }
    process { $null = $AllObjects.AddRange($InputObject) }
    end {
        $text = $AllObjects |
            Write-PowerShellHashtable
        $text |
            Set-Content -Path $DataFile
        Get-Item -Path $DataFile
    }
}
function Find-EnumsList {
    [CmdletBinding()]
    param ([string] $ProjectPath)
    if ($PSEdition -eq 'Core') { $Enums = Get-ChildItem -Path $ProjectPath\Enums\*.ps1 -ErrorAction SilentlyContinue -FollowSymlink } else { $Enums = Get-ChildItem -Path $ProjectPath\Enums\*.ps1 -ErrorAction SilentlyContinue }
    $Opening = '@('
    $Closing = ')'
    $Adding = ','
    $EnumsList = New-ArrayList
    Add-ToArray -List $EnumsList -Element $Opening
    Foreach ($import in @($Enums)) {
        $Entry = "'Enums\$($import.Name)'"
        Add-ToArray -List $EnumsList -Element $Entry
        Add-ToArray -List $EnumsList -Element $Adding
    }
    Remove-FromArray -List $EnumsList -LastElement
    Add-ToArray -List $EnumsList -Element $Closing
    return [string] $EnumsList
}
function Format-Code {
    [cmdletbinding()]
    param([string] $FilePath,
        $FormatCode)
    if ($FormatCode.Enabled) {
        if ($FormatCode.RemoveComments) { $Output = Write-TextWithTime -Text "Removing Comments - $FilePath" { Remove-Comments -FilePath $FilePath } } else { $Output = Write-TextWithTime -Text "Reading file content - $FilePath" { Get-Content -LiteralPath $FilePath -Raw } }
        if ($null -eq $FormatCode.FormatterSettings) { $FormatCode.FormatterSettings = $Script:FormatterSettings }
        $Output = Write-TextWithTime -Text "Formatting file - $FilePath" { try { Invoke-Formatter -ScriptDefinition $Output -Settings $FormatCode.FormatterSettings -Verbose:$false } catch {
                $ErrorMessage = $_.Exception.Message
                Write-Error "Format-Code - Formatting on file $FilePath failed. Error: $ErrorMessage"
                Exit
            } }
        $Output = foreach ($O in $Output) { if ($O.Trim() -ne '') { $O.Trim() } }
        Write-TextWithTime -Text "Saving file - $FilePath" { try { $Output | Out-File -LiteralPath $FilePath -NoNewline -Encoding utf8 } catch {
                $ErrorMessage = $_.Exception.Message
                Write-Error "Format-Code - Resaving file $FilePath failed. Error: $ErrorMessage"
                Exit
            } }
    }
}
function Format-PSD1 {
    [cmdletbinding()]
    param([string] $PSD1FilePath,
        $FormatCode)
    if ($FormatCode.Enabled) {
        $Output = Get-Content -LiteralPath $PSD1FilePath -Raw
        if ($FormatCode.RemoveComments) {
            Write-Verbose "Removing Comments - $PSD1FilePath"
            $Output = Remove-Comments -ScriptContent $Output
        }
        Write-Verbose "Formatting - $PSD1FilePath"
        if ($null -eq $FormatCode.FormatterSettings) { $FormatCode.FormatterSettings = $Script:FormatterSettings }
        $Output = Invoke-Formatter -ScriptDefinition $Output -Settings $FormatCode.FormatterSettings
        $Output | Out-File -LiteralPath $PSD1FilePath -NoNewline
    }
}
function Format-UsingNamespace {
    [CmdletBinding()]
    param([string] $FilePath,
        [string] $FilePathSave,
        [string] $FilePathUsing)
    if ($FilePathSave -eq '') { $FilePathSave = $FilePath }
    $FileStream = New-Object -TypeName IO.FileStream -ArgumentList ($FilePath), ([System.IO.FileMode]::Open), ([System.IO.FileAccess]::Read), ([System.IO.FileShare]::ReadWrite)
    $ReadFile = New-Object -TypeName System.IO.StreamReader -ArgumentList ($FileStream, [System.Text.Encoding]::UTF8, $true)
    $UsingNamespaces = [System.Collections.Generic.List[string]]::new()
    $Content = while (!$ReadFile.EndOfStream) {
        $Line = $ReadFile.ReadLine()
        if ($Line -like 'using namespace*') { $UsingNamespaces.Add($Line) } else { $Line }
    }
    $ReadFile.Close()
    $null = New-Item -Path $FilePathSave -ItemType file -Force
    if ($UsingNamespaces) {
        $null = New-Item -Path $FilePathUsing -ItemType file -Force
        $UsingNamespaces = $UsingNamespaces.Trim() | Sort-Object -Unique
        $UsingNamespaces | Add-Content -LiteralPath $FilePathUsing -Encoding utf8
        $Content | Add-Content -LiteralPath $FilePathSave -Encoding utf8
        return $true
    } else {
        $Content | Add-Content -LiteralPath $FilePathSave -Encoding utf8
        return $False
    }
}
Function Get-AliasTarget {
    [cmdletbinding()]
    param ([Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [Alias('PSPath', 'FullName')]
        [string[]]$Path)
    process {
        foreach ($File in $Path) {
            $FileAst = [System.Management.Automation.Language.Parser]::ParseFile($File, [ref]$null, [ref]$null)
            $FunctionName = $FileAst.FindAll( { param ($ast)
                    $ast -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $true).Name
            $AliasDefinitions = $FileAst.FindAll( { param ($ast)
                    $ast -is [System.Management.Automation.Language.StringConstantExpressionAst] -And $ast.Value -match '(New|Set)-Alias' }, $true)
            $AliasTarget = $AliasDefinitions.Parent.CommandElements.Where( { $_.StringConstantType -eq 'BareWord' -and
                    $_.Value -notin ('New-Alias', 'Set-Alias', $FunctionName) }).Value
            $Attributes = $FileAst.FindAll( { param ($ast)
                    $ast -is [System.Management.Automation.Language.AttributeAst] }, $true)
            $AliasDefinitions = $Attributes.Where( { $_.TypeName.Name -eq 'Alias' -and $_.Parent -is [System.Management.Automation.Language.ParamBlockAst] })
            $AliasTarget += $AliasDefinitions.PositionalArguments.Value
            [PsCustomObject]@{Function = $FunctionName
                Alias = $AliasTarget
            }
        }
    }
}
function Get-FunctionAliases {
    [cmdletbinding()]
    param([string] $Path)
    Import-Module $Path -Force -Verbose:$False
    $Names = Get-FunctionNames -Path $Path
    $Aliases = foreach ($Name in $Names) { Get-Alias | Where-Object { $_.Definition -eq $Name } }
    return $Aliases
}
function Get-FunctionAliasesFromFolder {
    [cmdletbinding()]
    param([string] $FullProjectPath,
        [string[]] $Folder)
    foreach ($F in $Folder) {
        $Path = [IO.Path]::Combine($FullProjectPath, $F)
        if ($PSEdition -eq 'Core') { $Files = Get-ChildItem -Path $Path -File -Recurse -FollowSymlink } else { $Files = Get-ChildItem -Path $Path -File -Recurse }
        $AliasesToExport = foreach ($file in $Files) { Get-AliasTarget -Path $File.FullName | Select-Object -ExpandProperty Alias }
        $AliasesToExport
    }
}
function Get-FunctionNames {
    [cmdletbinding()]
    param([string] $Path,
        [switch] $Recurse)
    [Management.Automation.Language.Parser]::ParseFile((Resolve-Path $Path),
        [ref]$null,
        [ref]$null).FindAll( { param($c)$c -is [Management.Automation.Language.FunctionDefinitionAst] }, $Recurse).Name
}
function Get-FunctionNamesFromFolder {
    [cmdletbinding()]
    param([string] $FullProjectPath,
        [string[]] $Folder)
    $Files = foreach ($F in $Folder) {
        $Path = [IO.Path]::Combine($FullProjectPath, $F)
        if ($PSEdition -eq 'Core') { Get-ChildItem -Path $Path -File -Recurse -FollowSymlink } else { Get-ChildItem -Path $Path -File -Recurse }
    }
    $Files = $Files | Sort-Object -Unique
    $FunctionToExport = foreach ($file in $Files) { Get-FunctionNames -Path $File.FullName }
    $FunctionToExport
}
function Get-GitLog {
    [CmdLetBinding(DefaultParameterSetName = 'Default')]
    param ([Parameter(ParameterSetName = 'Default', Mandatory)]
        [Parameter(ParameterSetName = 'SourceTarget', Mandatory)]
        [ValidateScript( { Resolve-Path -Path $_ | Test-Path })]
        [string]$GitFolder,
        [Parameter(ParameterSetName = 'SourceTarget', Mandatory)]
        [string]$StartCommitId,
        [Parameter(ParameterSetName = 'SourceTarget')]
        [string]$EndCommitId = 'HEAD')
    Push-Location
    try {
        Set-Location -Path $GitFolder
        $GitCommand = Get-Command -Name git -ErrorAction Stop
    } catch { $PSCmdlet.ThrowTerminatingError($_) }
    if ($StartCommitId) { $GitLogCommand = '"{0}" log --oneline --format="%H`t%h`t%ai`t%an`t%ae`t%ci`t%cn`t%ce`t%s`t%f" {1}...{2} 2>&1' -f $GitCommand.Source, $StartCommitId, $EndCommitId } else { $GitLogCommand = '"{0}" log --oneline --format="%H`t%h`t%ai`t%an`t%ae`t%ci`t%cn`t%ce`t%s`t%f" 2>&1' -f $GitCommand.Source }
    Write-Verbose -Message $GitLogCommand
    $GitLog = Invoke-Expression -Command "& $GitLogCommand" -ErrorAction SilentlyContinue
    Pop-Location
    if ($GitLog[0] -notmatch 'fatal:') { $GitLog | ConvertFrom-Csv -Delimiter "`t" -Header 'CommitId', 'ShortCommitId', 'AuthorDate', 'AuthorName', 'AuthorEmail', 'CommitterDate', 'CommitterName', 'ComitterEmail', 'CommitMessage', 'SafeCommitMessage' } else { if ($GitLog[0] -like "fatal: ambiguous argument '*...*'*") { Write-Warning -Message 'Unknown revision. Please check the values for StartCommitId or EndCommitId; omit the parameters to retrieve the entire log.' } else { Write-Error -Category InvalidArgument -Message ($GitLog -join "`n") } }
}
Function Get-ScriptComments {
    <#
    .Synopsis
    Get comments from a PowerShell script file.
    .Description
    This command will use the AST parser to go through a PowerShell script, either a .ps1 or .psm1 file, and display only the comments.
    .Example
    PS C:\> get-scriptcomments c:\scripts\MyScript.ps1
    #>

    [cmdletbinding()]
    Param([Parameter(Position = 0, Mandatory, HelpMessage = "Enter the path of a PS1 file",
            ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [Alias("PSPath", "Name")]
        [ValidateScript( { Test-Path $_ })]
        [ValidatePattern("\.ps(1|m1)$")]
        [string]$Path)
    Begin {
        Write-Verbose -Message "Starting $($MyInvocation.Mycommand)"
        New-Variable astTokens -force
        New-Variable astErr -force
    }
    Process {
        $Path = Convert-Path -Path $Path
        Write-Verbose -Message "Parsing $Path"
        $ast = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$astTokens, [ref]$astErr)
        $asttokens.where( { $_.kind -eq 'comment' }) | Select-Object -ExpandProperty Text
        $ast
    }
    End { Write-Verbose -Message "Ending $($MyInvocation.Mycommand)" }
}
function Merge-Module {
    [CmdletBinding()]
    param ([string] $ModuleName,
        [string] $ModulePathSource,
        [string] $ModulePathTarget,
        [Parameter(Mandatory = $false, ValueFromPipeline = $false)]
        [ValidateSet("ASC", "DESC", "NONE")]
        [string] $Sort = 'NONE',
        [string[]] $FunctionsToExport,
        [string[]] $AliasesToExport,
        [Array] $LibrariesCore,
        [Array] $LibrariesDefault,
        [System.Collections.IDictionary] $FormatCodePSM1,
        [System.Collections.IDictionary] $FormatCodePSD1,
        [System.Collections.IDictionary] $Configuration)
    $PSM1FilePath = "$ModulePathTarget\$ModuleName.psm1"
    $PSD1FilePath = "$ModulePathTarget\$ModuleName.psd1"
    if ($PSEdition -eq 'Core') { $ScriptFunctions = Get-ChildItem -Path $ModulePathSource\*.ps1 -ErrorAction SilentlyContinue -Recurse -FollowSymlink } else { $ScriptFunctions = Get-ChildItem -Path $ModulePathSource\*.ps1 -ErrorAction SilentlyContinue -Recurse }
    if ($Sort -eq 'ASC') { $ScriptFunctions = $ScriptFunctions | Sort-Object -Property Name } elseif ($Sort -eq 'DESC') { $ScriptFunctions = $ScriptFunctions | Sort-Object -Descending -Property Name }
    foreach ($FilePath in $ScriptFunctions) {
        $Content = Get-Content -Path $FilePath -Raw
        $Content = $Content.Replace('$PSScriptRoot\', '$PSScriptRoot\')
        $Content = $Content.Replace('$PSScriptRoot\', '$PSScriptRoot\')
        try { $Content | Out-File -Append -LiteralPath $PSM1FilePath -Encoding utf8 } catch {
            $ErrorMessage = $_.Exception.Message
            Write-Error "Merge-Module - Merge on file $FilePath failed. Error: $ErrorMessage"
            Exit
        }
    }
    $FilePathUsing = "$ModulePathTarget\$ModuleName.Usings.ps1"
    $UsingInPlace = Format-UsingNamespace -FilePath $PSM1FilePath -FilePathUsing $FilePathUsing
    if ($UsingInPlace) {
        Format-Code -FilePath $FilePathUsing -FormatCode $FormatCodePSM1
        $Configuration.UsingInPlace = "$ModuleName.Usings.ps1"
    }
    New-PSMFile -Path $PSM1FilePath -FunctionNames $FunctionsToExport -FunctionAliaes $AliasesToExport -LibrariesCore $LibrariesCore -LibrariesDefault $LibrariesDefault -ModuleName $ModuleName -UsingNamespaces:$UsingInPlace
    Format-Code -FilePath $PSM1FilePath -FormatCode $FormatCodePSM1
    New-PersonalManifest -Configuration $Configuration -ManifestPath $PSD1FilePath -AddUsingsToProcess
    Format-Code -FilePath $PSD1FilePath -FormatCode $FormatCodePSD1
}
function New-CreateModule {
    [CmdletBinding()]
    param ([string] $ProjectName,
        [string] $ModulePath,
        [string] $ProjectPath)
    $FullProjectPath = "$projectPath\$projectName"
    $Folders = 'Private', 'Public', 'Examples', 'Ignore', 'Publish', 'Enums', 'Data'
    Add-Directory $FullProjectPath
    foreach ($folder in $Folders) { Add-Directory "$FullProjectPath\$folder" }
    Copy-File -Source "$PSScriptRoot\Data\Example-Gitignore.txt" -Destination "$FullProjectPath\.gitignore"
    Copy-File -Source "$PSScriptRoot\Data\Example-LicenseMIT.txt" -Destination "$FullProjectPath\License"
    Copy-File -Source "$PSScriptRoot\Data\Example-ModuleStarter.ps1" -Destination "$FullProjectPath\$ProjectName.psm1"
}
function New-PersonalManifest {
    [CmdletBinding()]
    param([System.Collections.IDictionary] $Configuration,
        [string] $ManifestPath,
        [switch] $AddScriptsToProcess,
        [switch] $AddUsingsToProcess)
    $Manifest = $Configuration.Information.Manifest
    $Manifest.Path = $ManifestPath
    if (-not $AddScriptsToProcess) { $Manifest.ScriptsToProcess = @() }
    if ($AddUsingsToProcess -and $Configuration.UsingInPlace) { $Manifest.ScriptsToProcess = @($Configuration.UsingInPlace) }
    New-ModuleManifest @Manifest
    if ($Configuration.Steps.PublishModule.Prerelease -ne '') {
        $Data = Import-PowerShellDataFile -Path $Configuration.Information.Manifest.Path
        if ($Data.ScriptsToProcess.Count -eq 0) { $Data.Remove('ScriptsToProcess') }
        if ($Data.CmdletsToExport.Count -eq 0) { $Data.Remove('CmdletsToExport') }
        $Data.PrivateData.PSData.Prerelease = $Configuration.Steps.PublishModule.Prerelease
        $Data | Export-PSData -DataFile $Configuration.Information.Manifest.Path
    }
    Write-TextWithTime -Text "Converting $($Configuration.Information.Manifest.Path) UTF8 without BOM" { (Get-Content $Manifest.Path) | Out-FileUtf8NoBom $Manifest.Path }
}
function New-PrepareManifest {
    [CmdletBinding()]
    param($ProjectName,
        $modulePath,
        $projectPath,
        $functionToExport,
        $projectUrl)
    Set-Location "$projectPath\$ProjectName"
    $manifest = @{Path = ".\$ProjectName.psd1"
        RootModule = "$ProjectName.psm1"
        Author = 'Przemyslaw Klys'
        CompanyName = 'Evotec'
        Copyright = 'Evotec (c) 2011-2019. All rights reserved.'
        Description = "Simple project"
        FunctionsToExport = $functionToExport
        CmdletsToExport = ''
        VariablesToExport = ''
        AliasesToExport = ''
        FileList = "$ProjectName.psm1", "$ProjectName.psd1"
        HelpInfoURI = $projectUrl
        ProjectUri = $projectUrl
    }
    New-ModuleManifest @manifest
}
function New-PrepareModule {
    [CmdletBinding()]
    param ([System.Collections.IDictionary] $Configuration)
    if (-not $Configuration) { return }
    $GlobalTime = [System.Diagnostics.Stopwatch]::StartNew()
    if (-not $Configuration.Information.DirectoryModulesCore) { $Configuration.Information.DirectoryModulesCore = "$Env:USERPROFILE\Documents\PowerShell\Modules" }
    if (-not $Configuration.Information.DirectoryModules) { $Configuration.Information.DirectoryModules = "$Env:USERPROFILE\Documents\WindowsPowerShell\Modules" }
    if ($Configuration.Steps.BuildModule.Enable) { Start-ModuleBuilding -Configuration $Configuration -Core:$false }
    if ($Configuration.Steps.BuildModule.EnableDesktop) { Start-ModuleBuilding -Configuration $Configuration -Core:$false }
    if ($Configuration.Steps.BuildModule.EnableCore) { Start-ModuleBuilding -Configuration $Configuration -Core:$true }
    $Execute = "$($GlobalTime.Elapsed.Days) days, $($GlobalTime.Elapsed.Hours) hours, $($GlobalTime.Elapsed.Minutes) minutes, $($GlobalTime.Elapsed.Seconds) seconds, $($GlobalTime.Elapsed.Milliseconds) milliseconds"
    Write-Host "[Module Building]" -NoNewline -ForegroundColor Cyan
    Write-Host "[Time Total: $Execute]" -ForegroundColor Green
}
function New-PSMFile {
    [cmdletbinding()]
    param([string] $Path,
        [string[]] $FunctionNames,
        [string[]] $FunctionAliaes,
        [Array] $LibrariesCore,
        [Array] $LibrariesDefault,
        [string] $ModuleName,
        [switch] $UsingNamespaces)
    try {
        if ($FunctionNames.Count -gt 0) {
            $Functions = ($FunctionNames | Sort-Object -Unique) -join "','"
            $Functions = "'$Functions'"
        } else { $Functions = @() }
        if ($FunctionAliaes.Count -gt 0) {
            $Aliases = ($FunctionAliaes | Sort-Object -Unique) -join "','"
            $Aliases = "'$Aliases'"
        } else { $Aliases = @() }
        "" | Add-Content -Path $Path
        if ($LibrariesCore.Count -gt 0 -and $LibrariesDefault.Count -gt 0) {
            'if ($PSEdition -eq ''Core'') {' | Add-Content -Path $Path
            foreach ($File in $LibrariesCore) {
                $Output = 'Add-Type -Path $PSScriptRoot\' + $File
                $Output | Add-Content -Path $Path
            }
            '} else {' | Add-Content -Path $Path
            foreach ($File in $LibrariesDefault) {
                $Output = 'Add-Type -Path $PSScriptRoot\' + $File
                $Output | Add-Content -Path $Path
            }
            '}' | Add-Content -Path $Path
        } elseif ($LibrariesCore.Count -gt 0) {
            foreach ($File in $LibrariesCore) {
                $Output = 'Add-Type -Path $PSScriptRoot\' + $File
                $Output | Add-Content -Path $Path
            }
        } elseif ($LibrariesDefault.Count -gt 0) {
            foreach ($File in $LibrariesDefault) {
                $Output = 'Add-Type -Path $PSScriptRoot\' + $File
                $Output | Add-Content -Path $Path
            }
        }
        @"
 
Export-ModuleMember ``
    -Function @($Functions) ``
    -Alias @($Aliases)
"@
 | Add-Content -Path $Path
} catch {
    $ErrorMessage = $_.Exception.Message
    Write-Error "New-PSM1File from $ModuleName failed build. Error: $ErrorMessage"
    Exit
}
}
function New-PublishModule {
    [cmdletbinding()]
    param($projectName,
        $apikey,
        [bool] $RequireForce)
    Publish-Module -Name $projectName -Repository PSGallery -NuGetApiKey $apikey -Force:$RequireForce -verbose
}
<#
.SYNOPSIS
  Outputs to a UTF-8-encoded file *without a BOM* (byte-order mark).
 
.DESCRIPTION
  Mimics the most important aspects of Out-File:
  * Input objects are sent to Out-String first.
  * -Append allows you to append to an existing file, -NoClobber prevents
    overwriting of an existing file.
  * -Width allows you to specify the line width for the text representations
     of input objects that aren't strings.
  However, it is not a complete implementation of all Out-String parameters:
  * Only a literal output path is supported, and only as a parameter.
  * -Force is not supported.
 
  Caveat: *All* pipeline input is buffered before writing output starts,
          but the string representations are generated and written to the target
          file one by one.
 
.NOTES
  The raison d'être for this advanced function is that, as of PowerShell v5,
  Out-File still lacks the ability to write UTF-8 files without a BOM:
  using -Encoding UTF8 invariably prepends a BOM.
 
#>

function Out-FileUtf8NoBom {
    [CmdletBinding()]
    param([Parameter(Mandatory, Position = 0)] [string] $LiteralPath,
        [switch] $Append,
        [switch] $NoClobber,
        [AllowNull()] [int] $Width,
        [Parameter(ValueFromPipeline)] $InputObject)
    [System.IO.Directory]::SetCurrentDirectory($PWD)
    $LiteralPath = [IO.Path]::GetFullPath($LiteralPath)
    if ($NoClobber -and (Test-Path $LiteralPath)) { Throw [IO.IOException] "The file '$LiteralPath' already exists." }
    $sw = New-Object IO.StreamWriter $LiteralPath, $Append
    $htOutStringArgs = @{ }
    if ($Width) { $htOutStringArgs += @{Width = $Width }
    }
    try { $Input | Out-String -Stream @htOutStringArgs | ForEach-Object { $sw.WriteLine($_) } } finally { $sw.Dispose() }
}
function Remove-Comments {
    Param ([string] $FilePath,
        [parameter(ValueFromPipeline = $True)] $Scriptblock,
        [string] $ScriptContent)
    if ($PSBoundParameters['FilePath']) {
        $ScriptBlockString = [IO.File]::ReadAllText((Resolve-Path $FilePath))
        $ScriptBlock = [ScriptBlock]::Create($ScriptBlockString)
    } elseif ($PSBoundParameters['ScriptContent']) { $ScriptBlock = [ScriptBlock]::Create($ScriptContent) } else { }
    $OldScript = $ScriptBlock -join [environment]::NewLine
    If (-not $OldScript.Trim(" `n`r`t")) { return }
    $Tokens = [System.Management.Automation.PSParser]::Tokenize($OldScript, [ref]$Null)
    $AllowedComments = @('requires'
        '.SYNOPSIS'
        '.DESCRIPTION'
        '.PARAMETER'
        '.EXAMPLE'
        '.INPUTS'
        '.OUTPUTS'
        '.NOTES'
        '.LINK'
        '.COMPONENT'
        '.ROLE'
        '.FUNCTIONALITY'
        '.FORWARDHELPCATEGORY'
        '.REMOTEHELPRUNSPACE'
        '.EXTERNALHELP')
    $Tokens = $Tokens.ForEach{ If ($_.Type -ne 'Comment') { $_ } Else {
            $CommentText = $_.Content.Substring($_.Content.IndexOf('#') + 1)
            $FirstInnerToken = [System.Management.Automation.PSParser]::Tokenize($CommentText, [ref]$Null) |
                Where-Object { $_.Type -ne 'NewLine' } |
                    Select-Object -First 1
            If ($FirstInnerToken.Content -in $AllowedComments) { $_ }
        } }
    $SkipNext = $False
    $ScriptProcessing = @(If ($Tokens.Count -gt 1) {
            ForEach ($i in (0..($Tokens.Count - 2))) {
                If (-not $SkipNext -and
                    $Tokens[$i ].Type -ne 'LineContinuation' -and ($Tokens[$i ].Type -notin ('NewLine', 'StatementSeparator') -or
                        $Tokens[$i + 1].Type -notin ('NewLine', 'StatementSeparator', 'GroupEnd'))) {
                    If ($Tokens[$i].Type -in ('String', 'Variable')) { $OldScript.Substring($Tokens[$i].Start, $Tokens[$i].Length) } Else { $Tokens[$i].Content }
                    If ($Tokens[$i ].Type -notin ('NewLine', 'GroupStart', 'StatementSeparator') -and
                        $Tokens[$i + 1].Type -notin ('NewLine', 'GroupEnd', 'StatementSeparator') -and
                        $Tokens[$i].EndLine -eq $Tokens[$i + 1].StartLine -and
                        $Tokens[$i + 1].StartColumn - $Tokens[$i].EndColumn -gt 0) { ' ' }
                    $SkipNext = $Tokens[$i].Type -eq 'GroupStart' -and $Tokens[$i + 1].Type -in ('NewLine', 'StatementSeparator')
                } Else { $SkipNext = $SkipNext -and $Tokens[$i + 1].Type -in ('NewLine', 'StatementSeparator') }
            }
        }
        If ($Tokens) { If ($Tokens[$i].Type -in ('String', 'Variable')) { $OldScript.Substring($Tokens[-1].Start, $Tokens[-1].Length) } Else { $Tokens[-1].Content } })
    [string] $NewScriptText = $ScriptProcessing -join ''
    $NewScriptText = $NewScriptText.TrimStart("`n`r;")
    If ($Scriptblock.Count -eq 1) { If ($Scriptblock[0] -is [scriptblock]) { return [scriptblock]::Create($NewScriptText) } Else { return $NewScriptText } } Else { return $NewScriptText.Split("`n`r", [System.StringSplitOptions]::RemoveEmptyEntries) }
}
function Remove-Directory {
    [CmdletBinding()]
    param ([string] $Dir)
    if (-not [string]::IsNullOrWhiteSpace($Dir)) {
        $exists = Test-Path -Path $Dir
        if ($exists) { Remove-Item $dir -Confirm:$false -Recurse } else { }
    }
}
$Script:FormatterSettings = @{IncludeRules = @('PSPlaceOpenBrace',
        'PSPlaceCloseBrace',
        'PSUseConsistentWhitespace',
        'PSUseConsistentIndentation',
        'PSAlignAssignmentStatement',
        'PSUseCorrectCasing')
    Rules = @{PSPlaceOpenBrace = @{Enable = $true
            OnSameLine = $true
            NewLineAfter = $true
            IgnoreOneLineBlock = $true
        }
        PSPlaceCloseBrace = @{Enable = $true
            NewLineAfter = $false
            IgnoreOneLineBlock = $true
            NoEmptyLineBefore = $false
        }
        PSUseConsistentIndentation = @{Enable = $true
            Kind = 'space'
            PipelineIndentation = 'IncreaseIndentationAfterEveryPipeline'
            IndentationSize = 4
        }
        PSUseConsistentWhitespace = @{Enable = $true
            CheckInnerBrace = $true
            CheckOpenBrace = $true
            CheckOpenParen = $true
            CheckOperator = $true
            CheckPipe = $true
            CheckSeparator = $true
        }
        PSUseCorrectCasing = @{Enable = $true }
    }
}
function Set-LinkedFiles {
    [CmdletBinding()]
    param([string[]] $LinkFiles,
        [string] $FullModulePath,
        [string] $FullProjectPath,
        [switch] $Delete)
    foreach ($file in $LinkFiles) {
        [string] $Path = "$FullModulePath\$file"
        [string] $Path2 = "$FullProjectPath\$file"
        if ($Delete) { if (Test-ReparsePoint -path $Path) { Remove-Item $Path -Confirm:$false } }
        $null = cmd /c mklink $path $path2
    }
}
function Start-ModuleBuilding {
    [CmdletBinding()]
    param([System.Collections.IDictionary] $Configuration,
        [switch] $Core)
    if ($Core) { [string] $FullModulePath = [IO.path]::Combine($Configuration.Information.DirectoryModulesCore, $Configuration.Information.ModuleName) } else { [string] $FullModulePath = [IO.path]::Combine($Configuration.Information.DirectoryModules, $Configuration.Information.ModuleName) }
    [string] $FullTemporaryPath = [IO.path]::GetTempPath() + '' + $Configuration.Information.ModuleName
    [string] $FullProjectPath = [IO.Path]::Combine($Configuration.Information.DirectoryProjects, $Configuration.Information.ModuleName)
    [string] $ProjectName = $Configuration.Information.ModuleName
    Write-Verbose '----------------------------------------------------'
    Write-Verbose "Project Name: $ProjectName"
    Write-Verbose "Full module path: $FullModulePath"
    Write-Verbose "Full project path: $FullProjectPath"
    Write-Verbose "Full module path to delete: $FullModulePathDelete"
    Write-Verbose "Full temporary path: $FullTemporaryPath"
    Write-Verbose "PSScriptRoot: $PSScriptRoot"
    Write-Verbose "PSEdition: $PSEdition"
    Write-Verbose '----------------------------------------------------'
    $CurrentLocation = (Get-Location).Path
    Set-Location -Path $FullProjectPath
    Remove-Directory $FullModulePath
    Remove-Directory $FullTemporaryPath
    Add-Directory $FullModulePath
    Add-Directory $FullTemporaryPath
    $DirectoryTypes = 'Public', 'Private', 'Lib', 'Bin', 'Enums', 'Images', 'Templates', 'Resources'
    $LinkDirectories = @()
    $LinkPrivatePublicFiles = @()
    $Configuration.Information.Manifest.RootModule = "$($ProjectName).psm1"
    $Configuration.Information.Manifest.FunctionsToExport = @()
    $Configuration.Information.Manifest.CmdletsToExport = @()
    $Configuration.Information.Manifest.VariablesToExport = @()
    $Configuration.Information.Manifest.AliasesToExport = @()
    if ($Configuration.Steps.BuildModule) {
        if ($PSEdition -eq 'core') {
            $Directories = Write-TextWithTime -Text "Getting directories list" { Get-ChildItem -Path $FullProjectPath -Directory -Recurse -FollowSymlink }
            $Files = Write-TextWithTime -Text "Getting files list" { Get-ChildItem -Path $FullProjectPath -File -Recurse -FollowSymlink }
            $FilesRoot = Write-TextWithTime -Text "Getting files list - root" { Get-ChildItem -Path $FullProjectPath -File -FollowSymlink }
        } else {
            $Directories = Write-TextWithTime -Text "Getting directories list" { Get-ChildItem -Path $FullProjectPath -Directory -Recurse }
            $Files = Write-TextWithTime -Text "Getting files list" { Get-ChildItem -Path $FullProjectPath -File -Recurse }
            $FilesRoot = Write-TextWithTime -Text "Getting files list - root" { Get-ChildItem -Path $FullProjectPath -File }
        }
        $LinkDirectories = Write-TextWithTime -Text "Adding Directories to Directory List" { foreach ($directory in $Directories) {
                $RelativeDirectoryPath = (Resolve-Path -LiteralPath $directory.FullName -Relative).Replace('.\', '')
                $RelativeDirectoryPath = "$RelativeDirectoryPath\"
                foreach ($LookupDir in $DirectoryTypes) { if ($RelativeDirectoryPath -like "$LookupDir\*") { $RelativeDirectoryPath } }
            } }
        $AllFiles = foreach ($File in $Files) {
            $RelativeFilePath = (Resolve-Path -LiteralPath $File.FullName -Relative).Replace('.\', '')
            $RelativeFilePath
        }
        $RootFiles = foreach ($File in $FilesRoot) {
            $RelativeFilePath = (Resolve-Path -LiteralPath $File.FullName -Relative).Replace('.\', '')
            $RelativeFilePath
        }
        $LinkFilesRoot = Write-TextWithTime -Text "Adding Files to Root Files List" { foreach ($File in $RootFiles | Sort-Object -Unique) {
                switch -Wildcard ($file) {
                    '*.psd1' { $File }
                    '*.psm1' { $File }
                    'License*' { $File }
                }
            } }
        $LinkPrivatePublicFiles = Write-TextWithTime -Text "Adding Files from subfolders" { foreach ($file in $AllFiles | Sort-Object -Unique) {
                switch -Wildcard ($file) {
                    '*.ps1' {
                        Add-FilesWithFolders -file $file -FullProjectPath $FullProjectPath -directory 'Private', 'Public', 'Enums'
                        continue
                    }
                    '*.*' {
                        Add-FilesWithFolders -file $file -FullProjectPath $FullProjectPath -directory 'Images\', 'Resources\', 'Templates\', 'Bin\', 'Lib\'
                        continue
                    }
                }
            } }
        $LinkPrivatePublicFiles = $LinkPrivatePublicFiles | Select-Object -Unique
        $Functions = Write-TextWithTime -Text 'Preparing functions to export' { Get-FunctionNamesFromFolder -FullProjectPath $FullProjectPath -Folder $Configuration.Information.FunctionsToExport }
        if ($Functions) { $Configuration.Information.Manifest.FunctionsToExport = $Functions }
        $Aliases = Write-TextWithTime -Text 'Preparing aliases' { Get-FunctionAliasesFromFolder -FullProjectPath $FullProjectPath -Folder $Configuration.Information.AliasesToExport }
        if ($Aliases) { $Configuration.Information.Manifest.AliasesToExport = $Aliases }
        if (-not [string]::IsNullOrWhiteSpace($Configuration.Information.ScriptsToProcess)) {
            $StartsWithEnums = "$($Configuration.Information.ScriptsToProcess)\"
            $FilesEnums = @($LinkPrivatePublicFiles | Where-Object { ($_).StartsWith($StartsWithEnums) })
            if ($FilesEnums.Count -gt 0) {
                Write-TextWithTime -Text "ScriptsToProcess export $FilesEnums"
                $Configuration.Information.Manifest.ScriptsToProcess = $FilesEnums
            }
        }
        $PSD1FilePath = "$FullProjectPath\$ProjectName.psd1"
        New-PersonalManifest -Configuration $Configuration -ManifestPath $PSD1FilePath -AddScriptsToProcess
        Format-Code -FilePath $PSD1FilePath -FormatCode $Configuration.Options.Standard.FormatCodePSD1
    }
    if ($Configuration.Steps.BuildModule.Merge) {
        foreach ($Directory in $LinkDirectories) {
            $Dir = "$FullTemporaryPath\$Directory"
            Add-Directory $Dir
        }
        $LinkDirectoriesWithSupportFiles = $LinkDirectories | Where-Object { $_ -ne 'Public\' -and $_ -ne 'Private\' }
        foreach ($Directory in $LinkDirectoriesWithSupportFiles) {
            $Dir = "$FullModulePath\$Directory"
            Add-Directory $Dir
        }
        Write-TextWithTime -Text "Linking files from Root Dir" { Set-LinkedFiles -LinkFiles $LinkFilesRoot -FullModulePath $FullTemporaryPath -FullProjectPath $FullProjectPath }
        Write-TextWithTime -Text "Linking files from Sub Dir" { Set-LinkedFiles -LinkFiles $LinkPrivatePublicFiles -FullModulePath $FullTemporaryPath -FullProjectPath $FullProjectPath }
        $FilesToLink = $LinkPrivatePublicFiles | Where-Object { $_ -notlike '*.ps1' -and $_ -notlike '*.psd1' }
        Set-LinkedFiles -LinkFiles $FilesToLink -FullModulePath $FullModulePath -FullProjectPath $FullProjectPath
        if (-not [string]::IsNullOrWhiteSpace($Configuration.Information.LibrariesCore)) {
            $StartsWithCore = "$($Configuration.Information.LibrariesCore)\"
            $FilesLibrariesCore = $LinkPrivatePublicFiles | Where-Object { ($_).StartsWith($StartsWithCore) }
        }
        if (-not [string]::IsNullOrWhiteSpace($Configuration.Information.LibrariesDefault)) {
            $StartsWithDefault = "$($Configuration.Information.LibrariesDefault)\"
            $FilesLibrariesDefault = $LinkPrivatePublicFiles | Where-Object { ($_).StartsWith($StartsWithDefault) }
        }
        Merge-Module -ModuleName $ProjectName -ModulePathSource $FullTemporaryPath -ModulePathTarget $FullModulePath -Sort $Configuration.Options.Merge.Sort -FunctionsToExport $Configuration.Information.Manifest.FunctionsToExport -AliasesToExport $Configuration.Information.Manifest.AliasesToExport -LibrariesCore $FilesLibrariesCore -LibrariesDefault $FilesLibrariesDefault -FormatCodePSM1 $Configuration.Options.Merge.FormatCodePSM1 -FormatCodePSD1 $Configuration.Options.Merge.FormatCodePSD1 -Configuration $Configuration
    } else {
        foreach ($Directory in $LinkDirectories) {
            $Dir = "$FullModulePath\$Directory"
            Add-Directory $Dir
        }
        Write-Verbose '[+] Linking files from Root Dir'
        Set-LinkedFiles -LinkFiles $LinkFilesRoot -FullModulePath $FullModulePath -FullProjectPath $FullProjectPath
        Write-Verbose '[+] Linking files from Sub Dir'
        Set-LinkedFiles -LinkFiles $LinkPrivatePublicFiles -FullModulePath $FullModulePath -FullProjectPath $FullProjectPath
    }
    if ($Configuration.Steps.PublishModule.Enabled) {
        if ($Configuration.Options.PowerShellGallery.FromFile) {
            $ApiKey = Get-Content -Path $Configuration.Options.PowerShellGallery.ApiKey
            New-PublishModule -ProjectName $Configuration.Information.ModuleName -ApiKey $ApiKey -RequireForce $Configuration.Steps.PublishModule.RequireForce
        } else { New-PublishModule -ProjectName $Configuration.Information.ModuleName -ApiKey $Configuration.Options.PowerShellGallery.ApiKey -RequireForce $Configuration.Steps.PublishModule.RequireForce }
    }
    Set-Location -Path $CurrentLocation
    if ($Configuration) {
        if ($Configuration.Options.ImportModules.RequiredModules) { Write-TextWithTime -Text 'Importing modules - REQUIRED' { foreach ($Module in $Configuration.Information.Manifest.RequiredModules) { Import-Module -Name $Module -Force -ErrorAction Stop -Verbose:$false } } }
        if ($Configuration.Options.ImportModules.Self) { Write-TextWithTime -Text 'Importing module - SELF' { Import-Module -Name $ProjectName -Force -ErrorAction Stop -Verbose:$false } }
        if ($Configuration.Steps.BuildDocumentation) {
            $DocumentationPath = "$FullProjectPath\$($Configuration.Options.Documentation.Path)"
            $ReadMePath = "$FullProjectPath\$($Configuration.Options.Documentation.PathReadme)"
            Write-Verbose "Generating documentation to $DocumentationPath with $ReadMePath"
            if (-not (Test-Path -Path $DocumentationPath)) { $null = New-Item -Path "$FullProjectPath\Docs" -ItemType Directory -Force }
            $Files = Get-ChildItem -Path $DocumentationPath
            if ($Files.Count -gt 0) { $null = Update-MarkdownHelpModule $DocumentationPath -RefreshModulePage -ModulePagePath $ReadMePath -ErrorAction Stop } else {
                $null = New-MarkdownHelp -Module $ProjectName -WithModulePage -OutputFolder $DocumentationPath -ErrorAction Stop
                $null = Move-Item -Path "$DocumentationPath\$ProjectName.md" -Destination $ReadMePath
                if ($Configuration.Options.Documentation.UpdateWhenNew) { $null = Update-MarkdownHelpModule $DocumentationPath -RefreshModulePage -ModulePagePath $ReadMePath -ErrorAction Stop }
            }
        }
    }
}
function Test-ReparsePoint {
    [CmdletBinding()]
    param ([string]$path)
    $file = Get-Item $path -Force -ea SilentlyContinue
    return [bool]($file.Attributes -band [IO.FileAttributes]::ReparsePoint)
}
function Write-PowerShellHashtable {
    [cmdletbinding()]
    <#
    .Synopsis
        Takes an creates a script to recreate a hashtable
    .Description
        Allows you to take a hashtable and create a hashtable you would embed into a script.
 
        Handles nested hashtables and indents nested hashtables automatically.
    .Parameter inputObject
        The hashtable to turn into a script
    .Parameter scriptBlock
        Determines if a string or a scriptblock is returned
    .Example
        # Corrects the presentation of a PowerShell hashtable
        @{Foo='Bar';Baz='Bing';Boo=@{Bam='Blang'}} | Write-PowerShellHashtable
    .Outputs
        [string]
    .Outputs
        [ScriptBlock]
    .Link
        https://github.com/StartAutomating/Pipeworks
        about_hash_tables
    #>

    [OutputType([string], [ScriptBlock])]
    param([Parameter(Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [PSObject]
        $InputObject,
        [Alias('ScriptBlock')]
        [switch]$AsScriptBlock,
        [Switch]$Sort)
    process {
        $callstack = @(foreach ($_ in (Get-PSCallStack)) { if ($_.Command -eq "Write-PowerShellHashtable") { $_ } })
        $depth = $callStack.Count
        if ($inputObject -isnot [Hashtable]) { $newInputObject = @{PSTypeName = @($inputobject.pstypenames)[-1] }
            foreach ($prop in $inputObject.psobject.properties) { $newInputObject[$prop.Name] = $prop.Value }
            $inputObject = $newInputObject
        }
        if ($inputObject -is [Hashtable]) {
            $scriptString = ""
            $indent = $depth * 4
            $scriptString += "@{
"

            $items = $inputObject.GetEnumerator()
            if ($Sort) { $items = $items | Sort-Object Key }
            foreach ($kv in $items) {
                $scriptString += " " * $indent
                $keyString = "$($kv.Key)"
                if ($keyString.IndexOfAny(" _.#-+:;()'!?^@#$%&".ToCharArray()) -ne -1) { if ($keyString.IndexOf("'") -ne -1) { $scriptString += "'$($keyString.Replace("'","''"))'=" } else { $scriptString += "'$keyString'=" } } elseif ($keyString) { $scriptString += "$keyString=" }
                $value = $kv.Value
                if ($value -is [string]) { $value = "'" + $value.Replace("'", "''").Replace("’", "’’").Replace("‘", "‘‘") + "'" } elseif ($value -is [ScriptBlock]) { $value = "{$value}" } elseif ($value -is [switch]) { $value = if ($value) { '$true' } else { '$false' } } elseif ($value -is [DateTime]) { $value = if ($value) { "[DateTime]'$($value.ToString("o"))'" } } elseif ($value -is [bool]) { $value = if ($value) { '$true' } else { '$false' } } elseif ($value -and $value.GetType -and ($value.GetType().IsArray -or $value -is [Collections.IList])) {
                    $value = foreach ($v in $value) { if ($v -is [Hashtable]) { Write-PowerShellHashtable $v } elseif ($v -is [Object] -and $v -isnot [string]) { Write-PowerShellHashtable $v } else { ("'" + "$v".Replace("'", "''").Replace("’", "’’").Replace("‘", "‘‘") + "'") } }
                    $oldOfs = $ofs
                    $ofs = ",$(' ' * ($indent + 4))"
                    $value = "$value"
                    $ofs = $oldOfs
                } elseif ($value -as [Hashtable[]]) {
                    $value = foreach ($v in $value) { Write-PowerShellHashtable $v }
                    $value = $value -join ","
                } elseif ($value -is [Hashtable]) { $value = "$(Write-PowerShellHashtable $value)" } elseif ($value -as [Double]) { $value = "$value" } else {
                    $valueString = "'$value'"
                    if ($valueString[0] -eq "'" -and
                        $valueString[1] -eq "@" -and
                        $valueString[2] -eq "{") { $value = Write-PowerShellHashtable -InputObject $value } else { $value = $valueString }
                }
                $scriptString += "$value
"

            }
            $scriptString += " " * ($depth - 1) * 4
            $scriptString += "}"
            if ($AsScriptBlock) { [ScriptBlock]::Create($scriptString) } else { $scriptString }
        }
    }
}
function Write-TextWithTime {
    [CmdletBinding()]
    param([ScriptBlock] $Content,
        [string] $Text,
        [ValidateSet('OneLiner', 'Array')][string] $Option = 'OneLiner',
        [switch] $Continue,
        [System.ConsoleColor] $Color = [System.ConsoleColor]::Cyan,
        [System.ConsoleColor] $ColorTime = [System.ConsoleColor]::Green)
    Write-Host "[$Text]" -NoNewline -ForegroundColor $Color
    $Time = [System.Diagnostics.Stopwatch]::StartNew()
    if ($null -ne $Content) { & $Content }
    if ($Option -eq 'Array') { $TimeToExecute = "$($Time.Elapsed.Days) days", "$($Time.Elapsed.Hours) hours", "$($Time.Elapsed.Minutes) minutes", "$($Time.Elapsed.Seconds) seconds", "$($Time.Elapsed.Milliseconds) milliseconds" } else { $TimeToExecute = "$($Time.Elapsed.Days) days, $($Time.Elapsed.Hours) hours, $($Time.Elapsed.Minutes) minutes, $($Time.Elapsed.Seconds) seconds, $($Time.Elapsed.Milliseconds) milliseconds" }
    Write-Host " [Time: $TimeToExecute]" -ForegroundColor $ColorTime
    if (-not $Continue) { $Time.Stop() }
}
Export-ModuleMember -Function @('Get-GitLog', 'New-PrepareModule') -Alias @()