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 @() |