PSModuleDevelopment.psm1
$script:ModuleRoot = $PSScriptRoot $script:ModuleVersion = (Import-PowerShellDataFile -Path "$($script:ModuleRoot)\PSModuleDevelopment.psd1").ModuleVersion # Detect whether at some level dotsourcing was enforced $script:doDotSource = Get-PSFConfigValue -FullName PSModuleDevelopment.Import.DoDotSource -Fallback $false if ($PSModuleDevelopment_dotsourcemodule) { $script:doDotSource = $true } <# Note on Resolve-Path: All paths are sent through Resolve-Path/Resolve-PSFPath in order to convert them to the correct path separator. This allows ignoring path separators throughout the import sequence, which could otherwise cause trouble depending on OS. Resolve-Path can only be used for paths that already exist, Resolve-PSFPath can accept that the last leaf my not exist. This is important when testing for paths. #> # Detect whether at some level loading individual module files, rather than the compiled module was enforced $importIndividualFiles = Get-PSFConfigValue -FullName PSModuleDevelopment.Import.IndividualFiles -Fallback $false if ($PSModuleDevelopment_importIndividualFiles) { $importIndividualFiles = $true } if (Test-Path (Resolve-PSFPath -Path "$($script:ModuleRoot)\..\.git" -SingleItem -NewChild)) { $importIndividualFiles = $true } if ("<was compiled>" -eq '<was not compiled>') { $importIndividualFiles = $true } function Import-ModuleFile { <# .SYNOPSIS Loads files into the module on module import. .DESCRIPTION This helper function is used during module initialization. It should always be dotsourced itself, in order to proper function. This provides a central location to react to files being imported, if later desired .PARAMETER Path The path to the file to load .EXAMPLE PS C:\> . Import-ModuleFile -File $function.FullName Imports the file stored in $function according to import policy #> [CmdletBinding()] param ( [string] $Path ) $resolvedPath = $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($Path).ProviderPath if ($doDotSource) { . $resolvedPath } else { $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create([io.file]::ReadAllText($resolvedPath))), $null, $null) } } #region Load individual files if ($importIndividualFiles) { # Execute Preimport actions . Import-ModuleFile -Path "$ModuleRoot\internal\scripts\preimport.ps1" # Import all internal functions foreach ($function in (Get-ChildItem "$ModuleRoot\internal\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)) { . Import-ModuleFile -Path $function.FullName } # Import all public functions foreach ($function in (Get-ChildItem "$ModuleRoot\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)) { . Import-ModuleFile -Path $function.FullName } # Execute Postimport actions . Import-ModuleFile -Path "$ModuleRoot\internal\scripts\postimport.ps1" # End it here, do not load compiled code below return } #endregion Load individual files #region Load compiled code foreach ($resolvedPath in (Resolve-PSFPath -Path "$($script:ModuleRoot)\en-us\*.psd1")) { $data = Import-PowerShellDataFile -Path $resolvedPath foreach ($key in $data.Keys) { [PSFramework.Localization.LocalizationHost]::Write('PSModuleDevelopment', $key, 'en-US', $data[$key]) } } if ($IsLinux -or $IsMacOs) { # Defaults to the first value in $Env:XDG_CONFIG_DIRS on Linux or MacOS (or $HOME/.local/share/) $fileUserShared = @($Env:XDG_CONFIG_DIRS -split ([IO.Path]::PathSeparator))[0] if (-not $fileUserShared) { $fileUserShared = Join-Path $HOME .local/share/ } $path_FileUserShared = Join-Path (Join-Path $fileUserShared $psVersionName) "PSFramework" } else { # Defaults to $Env:AppData on Windows $path_FileUserShared = Join-Path $Env:AppData "$psVersionName\PSFramework\Config" if (-not $Env:AppData) { $path_FileUserShared = Join-Path ([Environment]::GetFolderPath("ApplicationData")) "$psVersionName\PSFramework\Config" } } # Store of registered build actions $script:buildActions = @{ } $script:buildArtifacts = @{ } Set-PSFConfig -Module PSModuleDevelopment -Name 'Build.Project.Selected' -Value '' -Validation string -Initialize -Description 'Path of the selected build project. Used when running Invoke-PSMDBuildProject without specifying a build file.' Set-PSFConfig -Module PSModuleDevelopment -Name 'Debug.ConfigPath' -Value (Join-Path -Path (Get-PSFPath -Name AppData) -ChildPath "InfernalAssociates/PowerShell/PSModuleDevelopment/config.xml") -Initialize -Validation string -Description 'The path to where the module debugging information is being stored. Used in the *-PSMDModuleDebug commands.' Set-PSFConfig -Module PSModuleDevelopment -Name 'Script.StagingRepository' -Value PSGallery -Validation string -Initialize -Description 'Repository to use for modules that are then retrieved when deploying a script' Set-PSFConfig -Module PSModuleDevelopment -Name 'Script.OutPath' -Value "$env:USERPROFILE\Desktop" -Validation string -Initialize -Description 'Default path where scripts are published to.' # The parameter identifier used to detect and insert parameters Set-PSFConfig -Module 'PSModuleDevelopment' -Name 'Template.Identifier' -Value 'þ' -Initialize -Validation 'string' -Description "The identifier used by the template system to detect and insert variables / scriptblock values" # The default values for common parameters Set-PSFConfig -Module 'PSModuleDevelopment' -Name 'Template.ParameterDefault.Author' -Value "$env:USERNAME" -Initialize -Validation 'string' -Description "The default value to set for the parameter 'Author'. This same setting can be created for any other parameter name." Set-PSFConfig -Module 'PSModuleDevelopment' -Name 'Template.ParameterDefault.Company' -Value "MyCompany" -Initialize -Validation 'string' -Description "The default value to set for the parameter 'Company'. This same setting can be created for any other parameter name." # The file extensions that will not be scanned for content replacement and will be stored as bytes Set-PSFConfig -Module 'PSModuleDevelopment' -Name 'Template.BinaryExtensions' -Value @('.dll', '.exe', '.pdf', '.doc', '.docx', '.xls', '.xlsx','.png','.ico','.bmp','.jpg','.jpeg','.pdb') -Initialize -Description "When creating a template, files with these extensions will be included as raw bytes and not interpreted for parameter insertion." # Define the default store. To add more stores, just add a similar setting with a different last name segment Set-PSFConfig -Module 'PSModuleDevelopment' -Name 'Template.Store.Default' -Value (Join-Path -Path (Get-PSFPath -Name AppData) -ChildPath "WindowsPowerShell/PSModuleDevelopment/Templates") -Initialize -Validation "string" -Description "Path to the default directory where PSModuleDevelopment will store its templates. You can add additional stores by creating the same setting again, only changing the last name segment to a new name and configuring a separate path." Set-PSFConfig -Module 'PSModuleDevelopment' -Name 'Template.Store.PSModuleDevelopment' -Value "$script:ModuleRoot/internal/templates" -Initialize -Validation "string" -Description "Path to the templates shipped in PSModuleDevelopment" # Define the default path to create from templates in Set-PSFConfig -Module 'PSModuleDevelopment' -Name 'Template.OutPath' -Value '.' -Initialize -Validation 'string' -Description "The path where new files & projects should be created from templates by default." Set-PSFConfig -Module PSModuleDevelopment -Name 'Module.Path' -Value "" -Initialize -Validation "string" -Handler { } -Description "The path to the module currently under development. Used as default path by commnds that work within a module directory." Set-PSFConfig -Module PSModuleDevelopment -Name 'Package.Path' -Value (Get-PSFPath -Name Temp) -Initialize -Validation "string" -Description "The default output path when exporting a module into a nuget package." Set-PSFConfig -Module PSModuleDevelopment -Name 'Find.DefaultExtensions' -Value '^\.ps1$|^\.psd1$|^\.psm1$|^\.cs$' -Initialize -Validation string -Description 'The pattern to use to select files to scan when using Find-PSMDFileContent.' Set-PSFConfig -Module PSModuleDevelopment -Name "ShowSyntax.ParmsNotFound" -Value "Red" -Initialize -Validation "string" -Handler { } -Description "The color to be used for the parameters that could not be found." Set-PSFConfig -Module PSModuleDevelopment -Name "ShowSyntax.CommandName" -Value "Green" -Initialize -Validation "string" -Handler { } -Description "The color to be used for the command name extracted from the command text." Set-PSFConfig -Module PSModuleDevelopment -Name "ShowSyntax.MandatoryParam" -Value "Yellow" -Initialize -Validation "string" -Handler { } -Description "The color to be used for the mandatory parameters from the commands parameter sets." Set-PSFConfig -Module PSModuleDevelopment -Name "ShowSyntax.NonMandatoryParam" -Value "DarkGray" -Initialize -Validation "string" -Handler { } -Description "The color to be used for the non mandatory parameters from the commands parameter sets." Set-PSFConfig -Module PSModuleDevelopment -Name "ShowSyntax.FoundAsterisk" -Value "Green" -Initialize -Validation "string" -Handler { } -Description "The color to be used for the asterisk that indicates a parameter has been filled / supplied." Set-PSFConfig -Module PSModuleDevelopment -Name "ShowSyntax.NotFoundAsterisk" -Value "Magenta" -Initialize -Validation "string" -Handler { } -Description "The color to be used for the asterisk that indicates a mandatory parameter has not been filled / supplied." Set-PSFConfig -Module PSModuleDevelopment -Name "ShowSyntax.ParmValue" -Value "DarkCyan" -Initialize -Validation "string" -Handler { } -Description "The color to be used for the parameter value." #Set-PSFConfig -Module PSModuleDevelopment -Name 'Wix.profile.path' -Value "$env:APPDATA\WindowsPowerShell\PSModuleDevelopment\Wix" -Initialize -Validation "string" -Handler { } -Description "The path where the wix commands store and look for msi building profiles by default." #Set-PSFConfig -Module PSModuleDevelopment -Name 'Wix.profile.default' -Value " " -Initialize -Validation "string" -Handler { } -Description "The default profile to build. If this is specified, 'Invoke-PSMDWixBuild' will build this profile when nothing else is specified." #region Ensure Config path exists # If the folder doesn't exist yet, create it $root = Split-Path (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Debug.ConfigPath') if (-not (Test-Path $root)) { New-Item $root -ItemType Directory -Force | Out-Null } # If the config file doesn't exist yet, create it if (-not (Test-Path (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Debug.ConfigPath'))) { Export-Clixml -InputObject @() -Path (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Debug.ConfigPath') } #endregion Ensure Config path exists # Pass on the host UI to the library [PSModuleDevelopment.Utility.UtilityHost]::RawUI = $host.UI.RawUI # Register Type-Conversion to fix template issues in serialization edge-casaes Register-PSFArgumentTransformationScriptblock -Name 'PSModuleDevelopment.TemplateItem' -Scriptblock { if ($_ -is [PSModuleDevelopment.Template.TemplateItemBase]) { return $_ } [PSModuleDevelopment.Template.TemplateHost]::GetTemplateItem($_) } function Export-PsmdBuildProjectFile { <# .SYNOPSIS Exports a build project object to file. .DESCRIPTION Exports a build project object to file. Strips out all superfluous properties on steps to improve readability of output. .PARAMETER OutPath The path to write the file to. .PARAMETER ProjectObject The build project to export. .EXAMPLE PS C:\> $projectObject | Export-PsmdBuildProjectFile -OutPath $outPath Exports the specified build project object to file. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $OutPath, [Parameter(Mandatory = $true, ValueFromPipeline = $true)] $ProjectObject ) process { $steps = foreach ($step in $ProjectObject.Steps) { $newStep = $step | ConvertTo-PSFHashtable -Include Name, Weight, Action if ($step.Dependency) { $newStep.Dependency = $step.Dependency } if ($step.Parameters) { $parameters = $step.Parameters | ConvertTo-PSFHashtable if ($parameters.Count -gt 0) { $newStep.Parameters = $parameters } } if ($step.Condition -and $step.ConditionSet) { $newStep.Condition = $step.Condition $newStep.ConditionSet = $step.ConditionSet } [PSCustomObject]$newStep } $ProjectObject.Steps = $steps | Sort-Object Weight $ProjectObject | ConvertTo-Json -Depth 10 | Set-Content -Path $OutPath -Encoding UTF8 -ErrorAction Stop } } function Get-PsmdTemplateStore { <# .SYNOPSIS Returns the configured template stores, usually only default. .DESCRIPTION Returns the configured template stores, usually only default. Returns null if no matching store is available. .PARAMETER Filter Default: "*" The returned stores are filtered by this. .EXAMPLE PS C:\> Get-PsmdTemplateStore Returns all stores configured. .EXAMPLE PS C:\> Get-PsmdTemplateStore -Filter default Returns the default store only #> [CmdletBinding()] Param ( [string] $Filter = "*" ) process { Get-PSFConfig -FullName "PSModuleDevelopment.Template.Store.$Filter" | ForEach-Object { New-Object PSModuleDevelopment.Template.Store -Property @{ Path = $_.Value Name = $_.Name -replace "^.+\." } } } } function Resolve-TemplateParameter { <# .SYNOPSIS Resolves the parameters to invoke a template with. .DESCRIPTION Resolves the parameters to invoke a template with. This processes parameters with the following, ascending priority: - From Configuration (if specified) - From PSMD Configuration files (if specified) - From being explicitly specified on invocation Explicitly bound parameters will thus always win. .PARAMETER Path Path in which the template is being invoked. If this parameter is specified, it will search the path and all parent paths for PSMDConfig.psd1 files. Then read the settings from them, starting at the root path. The deeper the path, the later the settings are loaded, overwriting settings from parent folders in case of conflict. .PARAMETER Configuration The Configuration settings specified by the user. These take precedence over anything else. .PARAMETER FromConfiguration Whether to load configuration settings from configuration. .EXAMPLE PS C:\> Resolve-TemplateParameter -Path $resolvedPath -Configuration $Configuration -FromConfiguration Resolves all parameters for the current template. #> [OutputType([hashtable])] [CmdletBinding()] param ( [string] $Path, [hashtable] $Configuration = @{ }, [switch] $FromConfiguration ) process { $newConfiguration = @{ } if ($Path) { $currentPath = $Path $paths = while ($currentPath) { $currentPath $currentPath = Split-Path $currentPath } foreach ($rootPath in $paths | Sort-Object Length) { $configPath = Join-Path $rootPath 'PSMDConfig.psd1' if (-not (Test-Path -Path $configPath)) { continue } $cfg = Import-PSFPowerShellDataFile -Path $configPath if ($cfg -and $cfg -is [hashtable]) { foreach ($pair in $cfg.GetEnumerator()) { $newConfiguration[$pair.Key] = $pair.Value } } } } foreach ($pair in $Configuration.GetEnumerator()) { $newConfiguration[$pair.Key] = $pair.Value } if ($FromConfiguration) { foreach ($config in Get-PSFConfig -Module 'PSModuleDevelopment' -Name 'Template.ParameterDefault.*') { $cfgName = $config.Name -replace '^.+\.([^\.]+)$', '$1' if (-not $newConfiguration.ContainsKey($cfgName)) { $newConfiguration[$cfgName] = $config.Value } } } $newConfiguration } } function Expand-PSMDTypeName { <# .SYNOPSIS Returns the full name of the input object's type, as well as the name of the types it inherits from, recursively until System.Object. .DESCRIPTION Returns the full name of the input object's type, as well as the name of the types it inherits from, recursively until System.Object. .PARAMETER InputObject The object whose typename to expand. .EXAMPLE PS C:\> Expand-PSMDTypeName -InputObject "test" Returns the typenames for the string test ("System.String" and "System.Object") #> [CmdletBinding()] Param ( [Parameter(ValueFromPipeline = $true)] $InputObject ) process { foreach ($item in $InputObject) { if ($null -eq $item) { continue } $type = $item.GetType() if ($type.FullName -eq "System.RuntimeType") { $type = $item } $type.FullName while ($type.FullName -ne "System.Object") { $type = $type.BaseType $type.FullName } } } } function Find-PSMDType { <# .SYNOPSIS Searches assemblies for types. .DESCRIPTION This function searches the currently imported assemblies for a specific type. It is not inherently limited to public types however, and can search interna just as well. Can be used to scan for dependencies, to figure out what libraries one needs for a given type and what dependencies exist. .PARAMETER Name Default: "*" The name of the type to search for. Accepts wildcards. .PARAMETER FullName Default: "*" The FullName of the type to search for. Accepts wildcards. .PARAMETER Assembly Default: (Get-PSMDAssembly) The assemblies to search. By default, all loaded assemblies are searched. .PARAMETER Public Whether the type to find must be public. .PARAMETER Enum Whether the type to find must be an enumeration. .PARAMETER Static Whether the type to find must be static. .PARAMETER Implements Whether the type to find must implement this interface .PARAMETER InheritsFrom The type must directly inherit from this type. Accepts wildcards. .PARAMETER Attribute The type must have this attribute assigned. Accepts wildcards. .EXAMPLE Find-PSMDType -Name "*String*" Finds all types whose name includes the word "String" (This will be quite a few) .EXAMPLE Find-PSMDType -InheritsFrom System.Management.Automation.Runspaces.Runspace Finds all types that inherit from the Runspace class #> [Alias('ftype')] [CmdletBinding()] Param ( [string] $Name = "*", [string] $FullName = "*", [Parameter(ValueFromPipeline = $true)] [System.Reflection.Assembly[]] $Assembly = (Get-PSMDAssembly), [switch] $Public, [switch] $Enum, [switch] $Static, [string] $Implements, [string] $InheritsFrom, [string] $Attribute ) begin { $boundEnum = Test-PSFParameterBinding -ParameterName Enum $boundPublic = Test-PSFParameterBinding -ParameterName Public $boundStatic = Test-PSFParameterBinding -ParameterName Static } process { foreach ($item in $Assembly) { if ($boundPublic) { if ($Public) { $types = $item.ExportedTypes } else { # Empty Assemblies will error on this, which is not really an issue and can be safely ignored try { $types = $item.GetTypes() | Where-Object IsPublic -EQ $false } catch { Write-PSFMessage -Message "Failed to enumerate types on $item" -Level InternalComment -Tag 'fail','assembly','type','enumerate' -ErrorRecord $_ } } } else { # Empty Assemblies will error on this, which is not really an issue and can be safely ignored try { $types = $item.GetTypes() } catch { Write-PSFMessage -Message "Failed to enumerate types on $item" -Level InternalComment -Tag 'fail', 'assembly', 'type', 'enumerate' -ErrorRecord $_ } } foreach ($type in $types) { if ($type.Name -notlike $Name) { continue } if ($type.FullName -notlike $FullName) { continue } if ($Implements -and ($type.ImplementedInterfaces.Name -notcontains $Implements)) { continue } if ($boundEnum -and ($Enum -ne $type.IsEnum)) { continue } if ($InheritsFrom -and ($type.BaseType.FullName -notlike $InheritsFrom)) { continue } if ($Attribute -and ($type.CustomAttributes.AttributeType.Name -notlike $Attribute)) { continue } if ($boundStatic -and ($Static -ne ($type.IsAbstract -and $type.IsSealed))) { continue } $type } } } } function Get-PSMDAssembly { <# .SYNOPSIS Returns the assemblies currently loaded. .DESCRIPTION Returns the assemblies currently loaded. .PARAMETER Filter Default: * The name to filter by .EXAMPLE Get-PSMDAssembly Lists all imported libraries .EXAMPLE Get-PSMDAsssembly -Filter "Microsoft.*" Lists all imported libraries whose name starts with "Microsoft.". #> [CmdletBinding()] Param ( [string] $Filter = "*" ) process { [appdomain]::CurrentDomain.GetAssemblies() | Where-Object FullName -Like $Filter } } function Get-PSMDConstructor { <# .SYNOPSIS Returns information on the available constructors of a type. .DESCRIPTION Returns information on the available constructors of a type. Accepts any object as pipeline input: - if it's a type, it will retrieve its constructors. - If it's not a type, it will retrieve the constructor from the type of object passed Will not duplicate constructors if multiple objects of the same type are passed. In order to retrieve the constructor of an array, wrap it into another array. .PARAMETER InputObject The object the constructor of which should be retrieved. .PARAMETER NonPublic Show non-public constructors instead. .EXAMPLE Get-ChildItem | Get-PSMDConstructor Scans all objects in the given path, than tries to retrieve the constructor for each kind of object returned (generally, this will return the constructors for file and folder objects) .EXAMPLE Get-PSMDConstructor $result Returns the constructors of objects stored in $result #> [CmdletBinding()] Param ( [Parameter(ValueFromPipeline = $true)] $InputObject, [switch] $NonPublic ) begin { $processedTypes = @() } process { foreach ($item in $InputObject) { if ($null -eq $item) { continue } if ($item -is [System.Type]) { $type = $item } else { $type = $item.GetType() } if ($processedTypes -contains $type) { continue } if ($NonPublic) { foreach ($constructor in $type.GetConstructors([System.Reflection.BindingFlags]'NonPublic, Instance')) { New-Object PSModuleDevelopment.PsmdAssembly.Constructor($constructor) } } else { foreach ($constructor in $type.GetConstructors()) { New-Object PSModuleDevelopment.PsmdAssembly.Constructor($constructor) } } $processedTypes += $type } } } function Get-PSMDMember { <# .ForwardHelpTargetName Microsoft.PowerShell.Utility\Get-Member .ForwardHelpCategory Cmdlet #> [CmdletBinding(HelpUri = 'https://go.microsoft.com/fwlink/?LinkID=113322', RemotingCapability = 'None')] param ( [Parameter(ValueFromPipeline = $true)] [psobject] $InputObject, [Parameter(Position = 0)] [ValidateNotNullOrEmpty()] [string[]] $Name, [Alias('Type')] [System.Management.Automation.PSMemberTypes] $MemberType, [System.Management.Automation.PSMemberViewTypes] $View, [string] $ArgumentType, [string] $ReturnType, [switch] $Static, [switch] $Force ) begin { try { $outBuffer = $null if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer)) { $PSBoundParameters['OutBuffer'] = 1 } if ($ArgumentType) { $null = $PSBoundParameters.Remove("ArgumentType") } if ($ReturnType) { $null = $PSBoundParameters.Remove("ReturnType") } $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Utility\Get-Member', [System.Management.Automation.CommandTypes]::Cmdlet) $scriptCmd = { & $wrappedCmd @PSBoundParameters } $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin) $steppablePipeline.Begin($true) } catch { throw } function Split-Member { [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true)] [Microsoft.PowerShell.Commands.MemberDefinition] $Member ) process { if ($Member.MemberType -notlike "Method") { return $Member } if ($Member.Definition -notlike "*), *") { return $Member } foreach ($definition in $Member.Definition.Replace("), ", ")þþþ").Split("þþþ")) { if (-not $definition) { continue } New-Object Microsoft.PowerShell.Commands.MemberDefinition($Member.TypeName, $Member.Name, $Member.MemberType, $definition) } } } } process { try { $members = $steppablePipeline.Process($_) | Split-Member if ($ArgumentType) { $tempMembers = @() foreach ($member in $members) { if ($member.MemberType -notlike "Method") { continue } if (($member.Definition -split "\(",2)[1] -match $ArgumentType) { $tempMembers += $member } } $members = $tempMembers } if ($ReturnType) { $members = $members | Where-Object Definition -match "^$ReturnType" } $members } catch { throw } } end { try { $steppablePipeline.End() } catch { throw } } } function Get-PSMDBuildAction { <# .SYNOPSIS Get a list of registered build actions. .DESCRIPTION Get a list of registered build actions. Actions are the scriptblocks that are used to execute the build logic when running Invoke-PSMDBuildProject. .PARAMETER Name The name by which to filter the actions returned. Defaults to '*' .EXAMPLE PS C:\> Get-PSMDBuildAction Get a list of all registered build actions. #> [CmdletBinding()] param ( [PsfArgumentCompleter('PSModuleDevelopment.Build.Action')] [string] $Name = '*' ) process { $script:buildActions.Values | Where-Object Name -Like $Name } } function Get-PSMDBuildArtifact { <# .SYNOPSIS Retrieve an artifact during a build project's execution. .DESCRIPTION Retrieve an artifact during a build project's execution. These artifacts are usually created during such an execution and discarded once completed. .PARAMETER Name The name by which to search for artifacts. Defaults to '*' .PARAMETER Tag Search for artifacts by tag. Artifacts can receive tag for better categorization. When specifying multiple tags, any artifact containing at least one of them will be returned. .EXAMPLE PS C:\> Get-PSMDBuildArtifact List all available artifacts. .EXAMPLE PS C:\> Get-PSMDBuildArtifact -Name ReleasePath Returns the artifact named "ReleasePath" .EXAMPLE PS C:\> Get-PSMDBuildArtifact -Tag pssession Returns all artifacts with the tag "pssession" #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')] [CmdletBinding()] param ( [string] $Name = '*', [string[]] $Tag ) process { $artifacts = $script:buildArtifacts.Values | Where-Object Name -Like $Name | Where-Object { if (-not $Tag) { return $true } foreach ($tagName in $Tag) { if ($_.Tags -contains $Tag) { return $true } } return $false } $($artifacts) } } function Get-PSMDBuildProject { <# .SYNOPSIS Reads & returns a build project. .DESCRIPTION Reads & returns a build project. A build project is a container including the steps executed during the build. .PARAMETER Path Path to the build project file. May target the folder, in which case the -Name parameter must be specified. .PARAMETER Name The name of the build project to read. Use together with the -Path parameter only. Absolute file path assumed will be: "<Path>\<Name>.build.json" .PARAMETER Selected Rather than specifying the path to read from, return the currently selected build project. Use Select-PSMDBuildProject to select a build project as the default ("selected") project. .EXAMPLE PS C:\> Get-PSMDBuildProject -Path 'C:\code\project' -Name project Will load the build project stored in the file "C:\code\project\project.build.json" #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')] [CmdletBinding(DefaultParameterSetName = 'Path')] param ( [Parameter(Mandatory = $true, ParameterSetName = 'Path')] [string] $Path, [Parameter(ParameterSetName = 'Path')] [string] $Name, [Parameter(Mandatory = $true, ParameterSetName = 'Selected')] [switch] $Selected ) process { if ($Path) { $importPath = $Path if ($Name) { $importPath = Join-Path -Path $Path -ChildPath "$Name.build.json" } if (-not (Test-Path -Path $importPath)) { $importPath = Join-Path -Path $Path -ChildPath "$Name.build.psd1" } } else { $importPath = Get-PSFConfigValue -FullName 'PSModuleDevelopment.Build.Project.Selected' } if (-not (Test-Path -Path $importPath)) { throw "Path not found: $importPath" } if ($importPath -like "*.psd1") { Import-PSFPowerShellDataFile -Path $importPath } else { Get-Content -Path $importPath -Encoding UTF8 | ConvertFrom-Json } } } function Get-PSMDBuildStep { <# .SYNOPSIS Read the steps that are part of the specified build project. .DESCRIPTION Read the steps that are part of the specified build project. .PARAMETER Name The name by which to filter the steps returned. Defaults to '*' .PARAMETER BuildProject Path to the build project file to read from. Defaults to the currently selected project if available. Use Select-PSMDBuildProject to select a default project. .EXAMPLE PS C:\> Get-PSMDBuildStep Read all steps that are part of the default build project. .EXAMPLE PS C:\> Get-PSMDBuildStep -Name CreateSession -BuildProject C:\code\Project\Project.build.json Return the CreateSession step from the specified project file. #> [CmdletBinding()] param ( [string] $Name = '*', [string] $BuildProject ) begin { $projectPath = $BuildProject if (-not $projectPath) { $projectPath = Get-PSFConfigValue -FullName 'PSModuleDevelopment.Build.Project.Selected' } if (-not $projectPath) { throw "No Project path specified and none selected!" } if (-not (Test-Path -Path $projectPath)) { throw "Project file not found: $projectPath" } } process { $projectObject = Get-PSMDBuildProject -Path $projectPath $projectObject.Steps | Where-Object Name -Like $Name } } function Invoke-PSMDBuildProject { <# .SYNOPSIS Execute a build project. .DESCRIPTION Execute a build project. A build project is a configured chain of actions that have been configured in json. They will be processed in their specified order and allow manageable, configurable steps without having to reinvent the same action again and again. + Individual action types become available using Register-PSMDBuildAction. + Create new build projects using New-PSMDBuildProject + Set up steps taken during a build using Set-PSMDBuildStep + Select the default build project using Select-PSMDBuildProject .PARAMETER Path The path to the build project file to execute. Mandatory if no build project has been selected as the default project. Use the Select-PSMDBuildProject to define a default project (and optionally persist the choice across sessions) .PARAMETER InheritArtifacts Accept artifacts that were generated before ever executing this pipeline. By default, any artifacts previously provisioned are cleared on pipeline start. .PARAMETER RetainArtifacts Whether, after executing the project, its artifacts should be retained. By default, any artifacts created during a build project will be discarded upon project completion. Artifacts are similar to variables to the pipeline and can be used to pass data throughout the pipeline. + Use Publish-PSMDBuildArtifact to create a new artifact. + Use Get-PSMDBuildArtifact to access existing build artifacts. .EXAMPLE PS C:\> Invoke-PSMDBuildProject -Path .\VMDeployment.build.Json Execute the build file "VMDeployment.build.json" from the current folder .EXAMPLE PS C:\> build Execute the default build project. #> [Alias('build')] [CmdletBinding()] param ( [string] $Path, [switch] $InheritArtifacts, [switch] $RetainArtifacts ) begin { if (-not $InheritArtifacts) { $script:buildArtifacts = @{ } } $buildStatus = @{ } $projectPath = $Path if (-not $projectPath) { $projectPath = Get-PSFConfigValue -FullName 'PSModuleDevelopment.Build.Project.Selected' } if (-not $projectPath) { throw "No Project path specified and none selected!" } if (-not (Test-Path -Path $projectPath)) { throw "Project file not found: $projectPath" } function Write-StepResult { [CmdletBinding()] param ( [int] $Count, [ValidateSet('Success', 'Failed', 'ConditionNotMet', 'DependencyNotMet', 'BadAction')] [string] $Status, $StepObject, $Data, [hashtable] $BuildStatus, [string] $ContinueLabel ) $BuildStatus[$StepObject.Name] = $Status -eq 'Success' $paramWritePSFMessage = @{ Level = 'Warning' String = "Invoke-PSMDBuildProject.Step.$Status" } switch ($Status) { Failed { Write-PSFMessage @paramWritePSFMessage -StringValues $StepObject.Name, $StepObject.Action -ErrorRecord $Data } ConditionNotMet { Write-PSFMessage @paramWritePSFMessage -StringValues $StepObject.Name, $StepObject.Action, $StepObject.Condition } DependencyNotMet { Write-PSFMessage @paramWritePSFMessage -StringValues $StepObject.Name, $StepObject.Action, $Data } BadAction { Write-PSFMessage @paramWritePSFMessage -StringValues $StepObject.Name, $StepObject.Action } } [PSCustomObject]@{ PSTypeName = 'PSModuleDevelopment.Build.StepResult' Count = $Count Action = $StepObject.Action Status = $Status Step = $StepObject.Name Data = $Data } if ($ContinueLabel) { continue $ContinueLabel } } } process { $projectObject = Get-PSMDBuildProject -Path $projectPath $steps = $projectObject.Steps | Sort-Object { $_.Weight } # Might be a hashtable $count = 0 $stepResults = :main foreach ($step in $steps) { $count++ $resultDef = @{ Count = $count StepObject = $step BuildStatus = $buildStatus } Write-PSFMessage -Level Host -String 'Invoke-PSMDBuildProject.Step.Executing' -StringValues $count, $step.Name, $step.Action #region Validation $actionObject = $script:buildActions[$step.Action] if (-not $actionObject) { Write-StepResult @resultDef -Status BadAction -ContinueLabel main } foreach ($dependency in $step.Dependency) { if (-not $buildStatus[$dependency]) { Write-StepResult @resultDef -Status DependencyNotMet -Data $dependency -ContinueLabel main } } if ($step.Condition -and $step.ConditionSet) { $cModule, $cSetName = $step.ConditionSet -split " ", 2 $conditionSet = Get-PSFFilterConditionSet -Module $cModule -Name $cSetName if (-not $conditionSet) { Write-StepResult @resultDef -Status ConditionNotMet -ContinueLabel main } $filter = New-PSFFilter -Expression $step.Condition -ConditionSet $conditionSet if (-not $filter.Evaluate()) { Write-StepResult @resultDef -Status ConditionNotMet -ContinueLabel main } } #endregion Validation #region Execution $parameters = @{ RootPath = Split-Path -Path $projectPath Parameters = $step.Parameters | ConvertTo-PSFHashtable ProjectName = $projectObject.Name StepName = $step.Name ParametersFromArtifacts = $step.ParametersFromArtifacts | ConvertTo-PSFHashtable } if (-not $parameters.Parameters) { $parameters.Parameters = @{ } } if (-not $parameters.ParametersFromArtifacts) { $parameters.ParametersFromArtifacts = @{ } } # Resolve Parameters $parameters.Parameters = Resolve-PSMDBuildStepParameter -Parameters $parameters.Parameters -FromArtifacts $parameters.ParametersFromArtifacts -ProjectName $parameters.ProjectName -StepName $parameters.StepName try { $null = & $actionObject.Action $parameters } catch { Write-StepResult @resultDef -Status Failed -Data $_ -ContinueLabel main } Write-StepResult @resultDef -Status Success #endregion Execution } $stepResults } end { if (-not $RetainArtifacts) { $script:buildArtifacts = @{ } } } } function New-PSMDBuildProject { <# .SYNOPSIS Create a new build project file. .DESCRIPTION Create a new build project file. Build projects are used to configure a repeatable, managed set of steps that make up a workflow. It is designed with software build processes in mind, but can be used for pretty much anything that works in separate steps. See the help on Invoke-PSMDBuildProject for more details. NOTE: This is not the tool or component to create new PowerShell _code_ projects / repositories! To create a new PowerShell module project, instead run: Invoke-PSMDTemplate PSFProject .PARAMETER Name The name of the build project. .PARAMETER Path The path to the folder in which the build project file is created. Final path will be: "<Path>\<Name>.build.json" .PARAMETER Condition A condition - a filter expression - that must be met in order for the build to proceed. For more details on filter conditions, see the PSFramework documentation on its feature: https://psframework.org/documentation/documents/psframework/filters.html .PARAMETER ConditionSet The name of the condition set to use. This is part of the PSFramework filter system: https://psframework.org/documentation/documents/psframework/filters.html Specify as "<module> <conditionsetname>" format. Default Value: PSFramework Environment .PARAMETER NoSelect Do not select the newly created build project as the default project for the current session. By default, the newly created build project will be set as default project, in order to facilitate adding steps to it. Use Select-PSMDBuildProject to explicitly set a default project file. .PARAMETER Register Persist the newly created build project as default build project beyond the current session. By default, the newly created build project will already be set as default project, in order to facilitate adding steps to it. But ONLY for the current session. This parameter makes it remember in new PowerShell sessions as well. .EXAMPLE PS C:\> New-PSMDBuildProject -Name 'VMDeployment' -Path 'C:\Code\VMDeployment' Create a new build project named 'VMDeployment' in the folder 'C:\Code\VMDeployment' #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] [CmdletBinding(DefaultParameterSetName = 'default')] param ( [Parameter(Mandatory = $true)] [string] $Name, [Parameter(Mandatory = $true)] [PsfValidateScript('PSFramework.Validate.FSPath.Folder', ErrorString = 'PSFramework.Validate.FSPath.Folder')] [string] $Path, [string] $Condition, [string] $ConditionSet = 'PSFramework Environment', [Parameter(ParameterSetName = 'NoSelect')] [switch] $NoSelect, [Parameter(ParameterSetName = 'Register')] [switch] $Register ) process { $resolvedPath = Resolve-PSFPath -Path $Path $project = [pscustomobject]@{ Name = $Name Condition = $Condition ConditionSet = $ConditionSet Steps = @() } $outPath = Join-Path -Path $resolvedPath -ChildPath "$Name.build.Json" $project | Export-PsmdBuildProjectFile -OutPath $outPath -ErrorAction Stop if (-not $NoSelect) { Set-PSFConfig -Module PSModuleDevelopment -Name 'Build.Project.Selected' -Value $outPath if ($Register) { Register-PSFConfig -Module PSModuleDevelopment -Name 'Build.Project.Selected' } } } } function Publish-PSMDBuildArtifact { <# .SYNOPSIS Create a new artifact for the current build pipeline. .DESCRIPTION Create a new artifact for the current build pipeline. Use this create artifacts that are accessible in later steps in the pipeline. Usually, artifacts are deleted at the end of a build process. They are always cleared at the beginning of a new one. Artifacts are NOT persisted across PowerShell sessions. .PARAMETER Name Name of the Artifact to create. Technically there are no limits to which character to chose, but we strongly encourage restricting yourself to letters, numbers, dash, underscore and dot. .PARAMETER Value The value to assign to the artifact. .PARAMETER Tag Any tags to add to an artifact. Tags can be searched for in order to bulk-operate against all artifacts of that tag. For example, the "remove-pssession" action can remove all remoting sessions for all artifacts tagged as "pssession". .EXAMPLE PS C:\> Publish-PSMDBuildArtifact -Name 'session' -Value $session -Tag 'pssession' Publishes an artifact named "session" containing the content of $session that is tagged as a PowerShell remoting session. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Name, [Parameter(Mandatory = $true)] [AllowNull()] $Value, [string[]] $Tag = @() ) process { $script:buildArtifacts[$Name] = [pscustomobject]@{ PSTypeName = 'PSModuleDevelopment.Build.Artifact' Name = $Name Value = $Value Tags = $Tag } } } function Register-PSMDBuildAction { <# .SYNOPSIS Register a new action usable in build projects. .DESCRIPTION Register a new action usable in build projects. Actions are the actual implementation logic that turns the configuration in a build project file into ... well, actions. Anyway, these are basically named scriptblocks with some metadata. This command is used to provide all the builtin actions and can be used to freely define your own actions. Whenever you use a "script" action in your build projects, consider ... would it make a good configurable option valuable for other builds? If so, that might just mark the birth of the next action! .PARAMETER Name The name of the action. .PARAMETER Action The actual code implementing the action. Each action scriptblock will receive exactly one . .PARAMETER Description A description explaining what the action is all about. .PARAMETER Parameters The parameters the action accepts. Provider a hashtable, with the keys being the parameter names and the values being a description of its parameter. .EXAMPLE PS C:\> Register-PSMDBuildAction -Name 'script' -Action $actionCode -Description 'Execute a custom scriptfile as part of your workflow' -Parameters $parameters Creates / registers the action "script". #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Name, [Parameter(Mandatory = $true)] [ScriptBlock] $Action, [Parameter(Mandatory = $true)] [string] $Description, [Parameter(Mandatory = $true)] [hashtable] $Parameters ) process { $script:buildActions[$Name] = [pscustomobject]@{ PSTypeName = 'PSModuleDevelopment.Build.Action' Name = $Name Action = $Action Description = $Description Parameters = $Parameters } } } function Remove-PSMDBuildArtifact { <# .SYNOPSIS Removes an artifact from the build pipeline. .DESCRIPTION Removes an artifact from the build pipeline. Only interacts with the PSModuleDevelopment build system. .PARAMETER Name Name of the artifact to remove. .EXAMPLE PS C:\> Remove-PSMDBuildArtifact -Name 'session' Removes the artifact 'session' from the build pipeline. .EXAMPLE PS C:\> Get-PSMDBuildArtifact -Tag pssession | Remove-PSMDBuildArtifact Removes all artifacts with the 'pssession' tag from the build pipeline. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [string[]] $Name ) process{ foreach ($nameString in $Name) { $script:buildArtifacts.Remove($nameString) } } } function Resolve-PSMDBuildStepParameter { <# .SYNOPSIS Resolves and consolidates the overall parameters of a given step. .DESCRIPTION Resolves and consolidates the overall parameters of a given step. This ensures that individual actions do not have to implement manual resolution and complex conditionals. Sources of parameters: - Explicitly defined parameter in the step - Value from Artifacts - Value from Configuration (only if not otherwise sourced) - Value from implicit artifact resolution: Any value that is formatted like this: "%!NameOfArtifact!%" will be replaced with the value of the artifact of the same name. This supports wildcard resolution, so "%!Session.*!%" will resolve to all artifacts with a name starting with "Session." Configuration-driven parameters follow this name scheme: "PSModuleDevelopment.BuildParam.<project>.<step>.<parameterName>" For example: "PSModuleDevelopment.BuildParam.Admf.connect.credential" .PARAMETER Parameters The hashtable containing the currently specified parameters from the step configuration within the build project file. Only settings not already defined there are taken from configuration. .PARAMETER FromArtifacts The hashtable mapping parameters from artifacts. This allows dynamically assigning artifacts to parameters. .PARAMETER ProjectName The name of the project being executed. Supplementary parameters taken from configuration will pick up settings based on this name: "PSModuleDevelopment.BuildParam.<ProjectName>.<StepName>.*" .PARAMETER StepName The name of the step being executed. Supplementary parameters taken from configuration will pick up settings based on this name: "PSModuleDevelopment.BuildParam.<ProjectName>.<StepName>.*" .EXAMPLE PS C:\> Resolve-PSMDBuildStepParameter -Parameters $actualParameters -ProjectName VMDeployment -StepName 'Create Session' Adds parameters provided through configuration. #> [OutputType([hashtable])] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [hashtable] $Parameters, [Parameter(Mandatory = $true)] [hashtable] $FromArtifacts, [Parameter(Mandatory = $true)] [string] $ProjectName, [Parameter(Mandatory = $true)] [string] $StepName ) process { # Process parameters from Configuration $configObject = Select-PSFConfig -FullName "PSModuleDevelopment.BuildParam.$ProjectName.$StepName.*" foreach ($property in $configObject.PSObject.Properties) { if ($property.Name -in '_Name', '_FullName', '_Depth', '_Children') { continue } if ($Parameters.ContainsKey($property.Name)) { continue } $Parameters[$property.Name] = $property.Value } # Process parameters from Artifacts foreach ($pair in $FromArtifacts.GetEnumerator()) { $Parameters[$pair.Key] = (Get-PSMDBuildArtifact -Name $pair.Value).Value } # Resolve implicit artifact references foreach ($key in $($Parameters.Keys)) { if ($Parameters.$key -notlike '%!*!%') { continue } $artifactName = $Parameters.$key -replace '^%!(.+?)!%$', '$1' $Parameters[$Key] = (Get-PSMDBuildArtifact -Name $artifactName).Value } $Parameters } } function Select-PSMDBuildProject { <# .SYNOPSIS Set the specified build project as the default project. .DESCRIPTION Set the specified build project as the default project. This will have most other commands in this Component automatically use the specified project. .PARAMETER Path Path to the project file to pick. .PARAMETER Register Persist the choice as default build project file across PowerShell sessions. .EXAMPLE PS C:\> Select-PSMDBuildProject -Path 'c:\code\Project\Project.build.json' Sets the specified build project as the default project. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Path, [switch] $Register ) process { Invoke-PSFProtectedCommand -ActionString 'Select-PSMDBuildProject.Testing' -ActionStringValues $Path -ScriptBlock { $null = Get-PSMDBuildProject -Path $Path -ErrorAction Stop } -Target $Path -EnableException $true -PSCmdlet $PSCmdlet Set-PSFConfig -Module PSModuleDevelopment -Name 'Build.Project.Selected' -Value $Path if ($Register) { Register-PSFConfig -Module PSModuleDevelopment -Name 'Build.Project.Selected' } } } function Set-PSMDBuildStep { <# .SYNOPSIS Create or update a step from a build project. .DESCRIPTION Create or update a step from a build project. .PARAMETER Name The name of the step. All step names must be unique within a single build project. .PARAMETER Weight The weight of the step. Weight determines processing order, the lower the number the earlier it is executed. .PARAMETER Action The name of the action to execute. Use Get-PSMDBuildAction to get a list of available actions. .PARAMETER Parameters The parameters this action should take. See the action object to see a description of parameters, including which must be provided and which can be skipped. .PARAMETER Condition A PSFramework filter condition that must apply for this action to be executed successfully. Example Conditions: Elevated PS7Plus -and OSWindows More Details: https://psframework.org/documentation/documents/psframework/filters.html .PARAMETER ConditionSet The name of the condition set to use. This is part of the PSFramework filter system: https://psframework.org/documentation/documents/psframework/filters.html Specify as "<module> <conditionsetname>" format. Default Value: PSFramework Environment .PARAMETER Dependency Any other steps that must successfully finished in order for this step to execute. ALL of the listed steps must have succeeded, skipped steps do not count. .PARAMETER BuildProject The build project file to work against. Specify the full path to the build project file. This parameter can be skipped if a default project file has been defined. .EXAMPLE PS C:\> Set-PSMDBuildStep -Name 'Create Session' -Action new-pssession -Parameters @{ VMName = 'labdc1'; CredentialPath = "%ProjectRoot%\creds\labdc1.cred"; } Defines a new step named 'Create Session' using the 'new-pssession'-action. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Name, [int] $Weight, [PsfArgumentCompleter('PSModuleDevelopment.Build.Action')] [string] $Action, [hashtable] $Parameters, [string] $Condition, [string] $ConditionSet, [string[]] $Dependency, [string] $BuildProject ) begin { $projectPath = $BuildProject if (-not $projectPath) { $projectPath = Get-PSFConfigValue -FullName 'PSModuleDevelopment.Build.Project.Selected' } if (-not $projectPath) { throw "No Project path specified and none selected!" } if (-not (Test-Path -Path $projectPath)) { throw "Project file not found: $projectPath" } } process { $projectObject = Get-PSMDBuildProject -Path $projectPath | ConvertTo-PSFHashtable $stepObject = $projectObject.Steps | Where-Object Name -EQ $Name | ConvertTo-PSFHashtable if (-not $stepObject) { $stepObject = [pscustomobject]@{ PSTypeName = 'PSModuleDevelopment.Build.Step' Name = $Name Weight = 50 Action = '' Parameters = @{ } Condition = '' ConditionSet = 'PSFramework Environment' Dependency = @() } } if (Test-PSFParameterBinding -ParameterName Weight) { $stepObject.Weight = $Weight } if (Test-PSFParameterBinding -ParameterName Action) { $stepObject.Action = $Action } if (Test-PSFParameterBinding -ParameterName Parameters) { $stepObject.Parameters = $Parameters } if (Test-PSFParameterBinding -ParameterName Condition) { $stepObject.Condition = $Condition } if (Test-PSFParameterBinding -ParameterName ConditionSet) { $stepObject.ConditionSet = $ConditionSet } if (Test-PSFParameterBinding -ParameterName Dependency) { $stepObject.Dependency = $Dependency } if (-not $stepObject.Action) { throw "Failed to save Build Step $Name : No Action defined!" } $projectObject.Steps = @($projectObject.Steps | Where-Object Name -ne $Name) + @($stepObject) $projectObject | Export-PsmdBuildProjectFile -OutPath $projectPath -ErrorAction Stop } } function New-PSMDFormatTableDefinition { <# .SYNOPSIS Generates a format XML for the input type. .DESCRIPTION Generates a format XML for the input type. Currently, only tables are supported. Note: Loading format files has a measureable impact on module import PER FILE. For the sake of performance, you should only generate a single file for an entire module. You can generate all items in a single call (which will probably be messy on many types at a time) Or you can use the -Fragment parameter to create individual fragments, and combine them by passing those items again to this command (the final time without the -Fragment parameter). .PARAMETER InputObject The object that will be used to generate the format XML for. Will not duplicate its work if multiple object of the same type are passed. Accepts objects generated when using the -Fragment parameter, combining them into a single document. .PARAMETER IncludeProperty Only properties in this list will be included. .PARAMETER ExcludeProperty Only properties not in this list will be included. .PARAMETER IncludePropertyAttribute Only properties that have the specified attribute will be included. .PARAMETER ExcludePropertyAttribute Only properties that do NOT have the specified attribute will be included. .PARAMETER Fragment The function will only return a partial Format-XML object (an individual table definition per type). .PARAMETER DocumentName Adds a name to the document generated. Purely cosmetic. .PARAMETER SortColumns Enabling this will cause the command to sort columns alphabetically. Explicit order styles take precedence over alphabetic sorting. .PARAMETER ColumnOrder Specify a list of properties in the order they should appear. For properties with labels: Labels take precedence over selected propertyname. .PARAMETER ColumnOrderHash Allows explicitly defining the order of columns on a per-type basis. These hashtables need to have two properties: - Type: The name of the type it applies to (e.g.: "System.IO.FileInfo") - Properties: The list of properties in the order they should appear. Example: @{ Type = "System.IO.FileInfo"; Properties = "Name", "Length", "LastWriteTime" } This parameter takes precedence over ColumnOrder in instances where the processed typename is explicitly listed in a hashtable. .PARAMETER ColumnTransformations A complex parameter that allows customizing columns or even adding new ones. This parameter accepts a hashtable that can ... - Set column width - Set column alignment - Add a script column - Assign a label to a column It can be targeted by typename as well as propertyname. Possible properties (others will be ignored): Content | Type | Possible Hashtable Keys Filter: Typename | string | T / Type / TypeName / FilterViewName Filter: Property | string | C / Column / Name / P / Property / PropertyName Append | bool | A / Append ScriptBlock | script | S / Script / ScriptBlock Label | string | L / Label Width | int | W / Width Alignment | string | Align / Alignment Notes: - Append needs to be specified if a new column should be added if no property to override was found. Use this to add a completely new column with a ScriptBlock. - Alignment: Expects a string, can be any choice of "Left", "Center", "Right" Example: $transform = @{ Type = "System.IO.FileInfo" Append = $true Script = { "{0} | {1}" -f $_.Extension, $_.Length } Label = "Ext.Length" Align = "Left" } .EXAMPLE PS C:\> Get-ChildItem | New-PSMDFormatTableDefinition Generates a format xml for the objects in the current path (files and folders in most cases) .EXAMPLE PS C:\> Get-ChildItem | New-PSMDFormatTableDefinition -IncludeProperty LastWriteTime, FullName Creates a format xml that only includes the columns LastWriteTime, FullName #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "")] [OutputType([PSModuleDevelopment.Format.Document], ParameterSetName = "default")] [OutputType([PSModuleDevelopment.Format.TableDefinition], ParameterSetName = "fragment")] [CmdletBinding(DefaultParameterSetName = "default")] Param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] $InputObject, [string[]] $IncludeProperty, [string[]] $ExcludeProperty, [string] $IncludePropertyAttribute, [string] $ExcludePropertyAttribute, [Parameter(ParameterSetName = "fragment")] [switch] $Fragment, [Parameter(ParameterSetName = "default")] [string] $DocumentName, [switch] $SortColumns, [string[]] $ColumnOrder, [hashtable[]] $ColumnOrderHash, [PSModuleDevelopment.Format.ColumnTransformation[]] $ColumnTransformations ) begin { $typeNames = @() $document = New-Object PSModuleDevelopment.Format.Document $document.Name = $DocumentName } process { foreach ($object in $InputObject) { #region Input Type Processing if ($object -is [PSModuleDevelopment.Format.TableDefinition]) { if ($Fragment) { $object continue } else { $document.Views.Add($object) continue } } if ($object.PSObject.TypeNames[0] -in $typeNames) { continue } else { $typeNames += $object.PSObject.TypeNames[0] } $typeName = $object.PSObject.TypeNames[0] #endregion Input Type Processing #region Process Properties $propertyNames = $object.PSOBject.Properties.Name if ($IncludeProperty) { $propertyNames = $propertyNames | Where-Object { $_ -in $IncludeProperty } } if ($ExcludeProperty) { $propertyNames = $propertyNames | Where-Object { $_ -notin $ExcludeProperty } } if ($IncludePropertyAttribute) { $listToInclude = @() $object.GetType().GetMembers([System.Reflection.BindingFlags]("FlattenHierarchy, Public, Instance")) | Where-Object { ($_.MemberType -match "Property|Field") -and ($_.CustomAttributes.AttributeType.Name -like $IncludePropertyAttribute) } | ForEach-Object { $listToInclude += $_.Name } $propertyNames = $propertyNames | Where-Object { $_ -in $listToInclude } } if ($ExcludePropertyAttribute) { $listToExclude = @() $object.GetType().GetMembers([System.Reflection.BindingFlags]("FlattenHierarchy, Public, Instance")) | Where-Object { ($_.MemberType -match "Property|Field") -and ($_.CustomAttributes.AttributeType.Name -like $ExcludePropertyAttribute) } | ForEach-Object { $listToExclude += $_.Name } $propertyNames = $propertyNames | Where-Object { $_ -notin $listToExclude } } $table = New-Object PSModuleDevelopment.Format.TableDefinition $table.Name = $typeName $table.ViewSelectedByType = $typeName foreach ($name in $propertyNames) { $column = New-Object PSModuleDevelopment.Format.Column $column.PropertyName = $name $table.Columns.Add($column) } foreach ($transform in $ColumnTransformations) { $table.TransformColumn($transform) } #endregion Process Properties #region Sort Columns if ($SortColumns) { $table.Columns.Sort() } $appliedOrder = $false foreach ($item in $ColumnOrderHash) { if (($item.Type -eq $typeName) -and ($item.Properties -as [string[]])) { [string[]]$props = $item.Properties $table.SetColumnOrder($props) $appliedOrder = $true } } if ((-not $appliedOrder) -and ($ColumnOrder)) { $table.SetColumnOrder($ColumnOrder) } #endregion Sort Columns $document.Views.Add($table) if ($Fragment) { $table } } } end { $document.Views.Sort() if (-not $Fragment) { $document } } } Function Get-PSMDHelp { <# .SYNOPSIS Displays localized information about Windows PowerShell commands and concepts. .DESCRIPTION The Get-PSMDHelp function is a wrapper around get-help that allows localizing help queries. This is especially useful when developing modules with help in multiple languages. .PARAMETER Category Displays help only for items in the specified category and their aliases. Valid values are Alias, Cmdlet, Function, Provider, Workflow, and HelpFile. Conceptual topics are in the HelpFile category. .PARAMETER Component Displays commands with the specified component value, such as "Exchange." Enter a component name. Wildcards are permitted. This parameter has no effect on displays of conceptual ("About_") help. .PARAMETER Detailed Adds parameter descriptions and examples to the basic help display. This parameter is effective only when help files are for the command are installed on the computer. It has no effect on displays of conceptual ("About_") help. .PARAMETER Examples Displays only the name, synopsis, and examples. To display only the examples, type "(Get-PSMDHelpEx <cmdlet-name>).Examples". This parameter is effective only when help files are for the command are installed on the computer. It has no effect on displays of conceptual ("About_") help. .PARAMETER Full Displays the entire help topic for a cmdlet, including parameter descriptions and attributes, examples, input and output object types, and additional notes. This parameter is effective only when help files are for the command are installed on the computer. It has no effect on displays of conceptual ("About_") help. .PARAMETER Functionality Displays help for items with the specified functionality. Enter the functionality. Wildcards are permitted. This parameter has no effect on displays of conceptual ("About_") help. .PARAMETER Name Gets help about the specified command or concept. Enter the name of a cmdlet, function, provider, script, or workflow, such as "Get-Member", a conceptual topic name, such as "about_Objects", or an alias, such as "ls". Wildcards are permitted in cmdlet and provider names, but you cannot use wildcards to find the names of function help and script help topics. To get help for a script that is not located in a path that is listed in the Path environment variable, type the path and file name of the script . If you enter the exact name of a help topic, Get-Help displays the topic contents. If you enter a word or word pattern that appears in several help topic titles, Get-Help displays a list of the matching titles. If you enter a word that does not match any help topic titles, Get-Help displays a list of topics that include that word in their contents. The names of conceptual topics, such as "about_Objects", must be entered in English, even in non-English versions of Windows PowerShell. .PARAMETER Language Set the language of the help returned. Use 5-digit language codes such as "en-us" or "de-de". Note: If PowerShell does not have help in the language specified, it will either return nothing or default back to English .PARAMETER SetLanguage Sets the language of the current and all subsequent help queries. Use 5-digit language codes such as "en-us" or "de-de". Note: If PowerShell does not have help in the language specified, it will either return nothing or default back to English .PARAMETER Online Displays the online version of a help topic in the default Internet browser. This parameter is valid only for cmdlet, function, workflow and script help topics. You cannot use the Online parameter in Get-Help commands in a remote session. For information about supporting this feature in help topics that you write, see about_Comment_Based_Help (http://go.microsoft.com/fwlink/?LinkID=144309), and "Supporting Online Help" (http://go.microsoft.com/fwlink/?LinkID=242132), and "How to Write Cmdlet Help" (http://go.microsoft.com/fwlink/?LinkID=123415) in the MSDN (Microsoft Developer Network) library. .PARAMETER Parameter Displays only the detailed descriptions of the specified parameters. Wildcards are permitted. This parameter has no effect on displays of conceptual ("About_") help. .PARAMETER Path Gets help that explains how the cmdlet works in the specified provider path. Enter a Windows PowerShell provider path. This parameter gets a customized version of a cmdlet help topic that explains how the cmdlet works in the specified Windows PowerShell provider path. This parameter is effective only for help about a provider cmdlet and only when the provider includes a custom version of the provider cmdlet help topic in its help file. To use this parameter, install the help file for the module that includes the provider. To see the custom cmdlet help for a provider path, go to the provider path location and enter a Get-Help command or, from any path location, use the Path parameter of Get-Help to specify the provider path. You can also find custom cmdlet help online in the provider help section of the help topics. For example, you can find help for the New-Item cmdlet in the Wsman:\*\ClientCertificate path (http://go.microsoft.com/fwlink/?LinkID=158676). For more information about Windows PowerShell providers, see about_Providers (http://go.microsoft.com/fwlink/?LinkID=113250). .PARAMETER Role Displays help customized for the specified user role. Enter a role. Wildcards are permitted. Enter the role that the user plays in an organization. Some cmdlets display different text in their help files based on the value of this parameter. This parameter has no effect on help for the core cmdlets. .PARAMETER ShowWindow Displays the help topic in a window for easier reading. The window includes a "Find" search feature and a "Settings" box that lets you set options for the display, including options to display only selected sections of a help topic. The ShowWindow parameter supports help topics for commands (cmdlets, functions, CIM commands, workflows, scripts) and conceptual "About" topics. It does not support provider help. This parameter is introduced in Windows PowerShell 3.0. .EXAMPLE PS C:\> Get-PSMDHelp Get-Help "en-us" -Detailed Gets the detailed help text of Get-Help in English #> [Alias('hex')] [CmdletBinding(DefaultParameterSetName = "AllUsersView")] Param ( [Parameter(ParameterSetName = "Parameters", Mandatory = $true)] [System.String] $Parameter, [Parameter(ParameterSetName = "Online", Mandatory = $true)] [System.Management.Automation.SwitchParameter] $Online, [Parameter(ParameterSetName = "ShowWindow", Mandatory = $true)] [System.Management.Automation.SwitchParameter] $ShowWindow, [Parameter(ParameterSetName = "AllUsersView")] [System.Management.Automation.SwitchParameter] $Full, [Parameter(ParameterSetName = "DetailedView", Mandatory = $true)] [System.Management.Automation.SwitchParameter] $Detailed, [Parameter(ParameterSetName = "Examples", Mandatory = $true)] [System.Management.Automation.SwitchParameter] $Examples, [ValidateSet("Alias", "Cmdlet", "Provider", "General", "FAQ", "Glossary", "HelpFile", "ScriptCommand", "Function", "Filter", "ExternalScript", "All", "DefaultHelp", "Workflow", "DscResource", "Class", "Configuration")] [System.String[]] $Category, [System.String[]] $Component, [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true)] [System.String] $Name, [Parameter(Position = 1)] [System.String] $Language, [System.String] $SetLanguage, [System.String] $Path, [System.String[]] $Functionality, [System.String[]] $Role ) Begin { if (Test-PSFParameterBinding -ParameterName "SetLanguage") { $script:set_language = $SetLanguage } if (Test-PSFParameterBinding -ParameterName "Language") { try { [System.Threading.Thread]::CurrentThread.CurrentUICulture = $Language } catch { Write-PSFMessage -Level Warning -Message "Failed to set language" -ErrorRecord $_ -Tag 'fail','language' } } elseif ($script:set_language) { try { [System.Threading.Thread]::CurrentThread.CurrentUICulture = $script:set_language } catch { Write-PSFMessage -Level Warning -Message "Failed to set language" -ErrorRecord $_ -Tag 'fail', 'language' } } # Prepare Splat for splatting a steppable pipeline $splat = $PSBoundParameters | ConvertTo-PSFHashtable -Exclude Language, SetLanguage try { $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Get-Help', [System.Management.Automation.CommandTypes]::Cmdlet) $scriptCmd = { & $wrappedCmd @splat } $steppablePipeline = $scriptCmd.GetSteppablePipeline() $steppablePipeline.Begin($PSCmdlet) } catch { throw } } Process { try { $steppablePipeline.Process($_) } catch { throw } } End { try { $steppablePipeline.End() } catch { throw } } } function Get-PSMDModuleDebug { <# .SYNOPSIS Retrieves module debugging configurations .DESCRIPTION Retrieves a list of all matching module debugging configurations. .PARAMETER Filter Default: "*" A string filter applied to the module name. All modules of matching name (using a -Like comparison) will be returned. .EXAMPLE PS C:\> Get-PSMDModuleDebug -Filter *net* Returns the module debugging configuration for all modules with a name that contains "net" #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')] [CmdletBinding()] Param ( [string] $Filter = "*" ) process { Import-Clixml -Path (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Debug.ConfigPath') | Where-Object { ($_.Name -like $Filter) -and ($_.Name.Length -gt 0) } } } function Import-PSMDModuleDebug { <# .SYNOPSIS Invokes the preconfigured import of a module. .DESCRIPTION Invokes the preconfigured import of a module. .PARAMETER Name The exact name of the module to import using the specified configuration. .EXAMPLE PS C:\> Import-PSMDModuleDebug -Name 'cPSNetwork' Imports the cPSNetwork module as it was configured to be imported using Set-ModuleDebug. #> [Alias('ipmod')] [CmdletBinding()] param ( [string] $Name ) process { # Get original module configuration $____module = $null $____module = Import-Clixml -Path (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Debug.ConfigPath') | Where-Object Name -eq $Name if (-not $____module) { throw "No matching module configuration found" } # Process entry if ($____module.DebugMode) { Set-Variable -Scope Global -Name "$($____module.Name)_DebugMode" -Value $____module.DebugMode -Force } if ($____module.PreImportAction) { [System.Management.Automation.ScriptBlock]::Create($____module.PreImportAction).Invoke() } Import-Module -Name $____module.Name -Scope Global if ($____module.PostImportAction) { [System.Management.Automation.ScriptBlock]::Create($____module.PostImportAction).Invoke() } } } function Remove-PSMDModuleDebug { <# .SYNOPSIS Removes module debugging configurations. .DESCRIPTION Removes module debugging configurations. .PARAMETER Name Name of modules whose debugging configuration should be removed. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .EXAMPLE PS C:\> Remove-PSMDModuleDebug -Name "cPSNetwork" Removes all module debugging configuration for the module cPSNetwork .NOTES Version 1.0.0.0 Author: Friedrich Weinmann Created on: August 7th, 2016 #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] Param ( [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 0, Mandatory = $true)] [string[]] $Name ) Begin { $allModules = Import-Clixml -Path (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Debug.ConfigPath') } Process { foreach ($nameItem in $Name) { ($allModules) | Where-Object { $_.Name -like $nameItem } | ForEach-Object { if (Test-PSFShouldProcess -Target $_.Name -Action 'Remove from list of modules configured for debugging' -PSCmdlet $PSCmdlet) { $Module = $_ $allModules = $allModules | Where-Object { $_ -ne $Module } } } } } End { Export-Clixml -InputObject $allModules -Path (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Debug.ConfigPath') -Depth 99 } } function Set-PSMDModuleDebug { <# .SYNOPSIS Configures how modules are handled during import of this module. .DESCRIPTION This module allows specifying other modules to import during import of this module. Using the Set-PSMDModuleDebug function it is possible to configure, which module is automatically imported, without having to edit the profile each time. This import occurs at the end of importing this module, thus setting this module in the profile as automatically imported is recommended. .PARAMETER Name The name of the module to configure for automatic import. Needs to be an exact match, the first entry found using "Get-Module -ListAvailable" will be imported. .PARAMETER AutoImport Setting this will cause the module to be automatically imported at the end of importing the PSModuleDevelopment module. Even when set to false, the configuration can still be maintained and the debug mode enabled. .PARAMETER DebugMode Setting this will cause the module to create a global variable named "<ModuleName>_DebugMode" with value $true during import of PSModuleDevelopment. Modules configured to use this variable can determine the intended import mode using this variable. .PARAMETER PreImportAction Any scriptblock that should run before importing the module. Only used when importing modules using the "Invoke-ModuleDebug" funtion, as is used for modules set to auto-import. .PARAMETER PostImportAction Any scriptblock that should run after importing the module. Only used when importing modules using the "Invoke-ModuleDebug" funtion, as his used for modules set to auto-import. .PARAMETER Priority When importing modules in a debugging context, they are imported in the order of their priority. The lower the number, the sooner it is imported. .PARAMETER AllAutoImport Changes all registered modules to automatically import on powershell launch. .PARAMETER NoneAutoImport Changes all registered modules to not automatically import on powershell launch. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .EXAMPLE PS C:\> Set-PSMDModuleDebug -Name 'cPSNetwork' -AutoImport Configures the module cPSNetwork to automatically import after importing PSModuleDevelopment .EXAMPLE PS C:\> Set-PSMDModuleDebug -Name 'cPSNetwork' -AutoImport -DebugMode Configures the module cPSNetwork to automatically import after importing PSModuleDevelopment using debug mode. .EXAMPLE PS C:\> Set-PSMDModuleDebug -Name 'cPSNetwork' -AutoImport -DebugMode -PreImportAction { Write-Host "Was done before importing" } -PostImportAction { Write-Host "Was done after importing" } Configures the module cPSNetwork to automatically import after importing PSModuleDevelopment using debug mode. - Running a scriptblock before import - Running another scriptblock after import Note: Using Write-Host is generally - but not always - bad practice Note: Verbose output during module import is generally discouraged (doesn't apply to tests of course) #> [Alias('smd')] [CmdletBinding(DefaultParameterSetName = "Name", SupportsShouldProcess = $true)] Param ( [Parameter(Mandatory = $true, Position = 0, ParameterSetName = "Name", ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('n')] [string] $Name, [Parameter(ParameterSetName = 'Name')] [Alias('ai')] [switch] $AutoImport, [Parameter(ParameterSetName = 'Name')] [Alias('dbg')] [switch] $DebugMode, [Parameter(ParameterSetName = 'Name')] [AllowNull()] [System.Management.Automation.ScriptBlock] $PreImportAction, [Parameter(ParameterSetName = 'Name')] [AllowNull()] [System.Management.Automation.ScriptBlock] $PostImportAction, [Parameter(ParameterSetName = 'Name')] [int] $Priority = 5, [Parameter(Mandatory = $true, ParameterSetName = 'AllImport')] [Alias('aai')] [switch] $AllAutoImport, [Parameter(Mandatory = $true, ParameterSetName = 'NoneImport')] [Alias('nai')] [switch] $NoneAutoImport ) process { #region AllAutoImport if ($AllAutoImport) { $allModules = Import-Clixml (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Debug.ConfigPath') if (Test-PSFShouldProcess -Target ($allModules.Name -join ", ") -Action "Configuring modules to automatically import" -PSCmdlet $PSCmdlet) { foreach ($module in $allModules) { $module.AutoImport = $true } Export-Clixml -InputObject $allModules -Path (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Debug.ConfigPath') } return } #endregion AllAutoImport #region NoneAutoImport if ($NoneAutoImport) { $allModules = Import-Clixml -Path (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Debug.ConfigPath') if (Test-PSFShouldProcess -Target ($allModules.Name -join ", ") -Action "Configuring modules to not automatically import" -PSCmdlet $PSCmdlet) { foreach ($module in $allModules) { $module.AutoImport = $false } Export-Clixml -InputObject $allModules -Path (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Debug.ConfigPath') } return } #endregion NoneAutoImport #region Name # Import all module-configurations $allModules = Import-Clixml -Path (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Debug.ConfigPath') # If a configuration already exists, change only those values that were specified if ($module = $allModules | Where-Object Name -eq $Name) { if (Test-PSFParameterBinding -ParameterName "AutoImport") { $module.AutoImport = $AutoImport.ToBool() } if (Test-PSFParameterBinding -ParameterName "DebugMode") { $module.DebugMode = $DebugMode.ToBool() } if (Test-PSFParameterBinding -ParameterName "PreImportAction") { $module.PreImportAction = $PreImportAction } if (Test-PSFParameterBinding -ParameterName "PostImportAction") { $module.PostImportAction = $PostImportAction } if (Test-PSFParameterBinding -ParameterName "Priority") { $module.Priority = $Priority } } # If no configuration exists yet, create a new one with all parameters as specified else { $module = [pscustomobject]@{ Name = $Name AutoImport = $AutoImport.ToBool() DebugMode = $DebugMode.ToBool() PreImportAction = $PreImportAction PostImportAction = $PostImportAction Priority = $Priority } } # Add new module configuration to all (if any) other previous configurations and export it to config file $newModules = @(($allModules | Where-Object Name -ne $Name), $module) if (Test-PSFShouldProcess -Target $name -Action "Changing debug settings for module" -PSCmdlet $PSCmdlet) { Export-Clixml -InputObject $newModules -Path (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Debug.ConfigPath') } #endregion Name } } function Measure-PSMDCommand { <# .SYNOPSIS Measures command performance with consecutive tests. .DESCRIPTION This function measures the performance of a scriptblock many consective times. Warning: Running a command repeatedly may not yield reliable information, since repeated executions may benefit from caching or other performance enhancing features, depending on the script content. This is best suited for measuring the performance of tasks that will later be run repeatedly as well. It also is useful for mitigating local performance fluctuations when comparing performances. .PARAMETER ScriptBlock The scriptblock whose performance is to be measure. .PARAMETER Iterations How many times should this performance test be repeated. .PARAMETER TestSet Accepts a hashtable, mapping a name to a specific scriptblock to measure. This will generate a result grading the performance of the various sets offered. .EXAMPLE PS C:\> Measure-PSMDCommand -ScriptBlock { dir \\Server\share } -Iterations 100 This tries to use Get-ChildItem on a remote directory 100 consecutive times, then measures performance and reports common performance indicators (Average duration, Maximum, Minimum, Total) #> [CmdletBinding()] param ( [Parameter(Mandatory = $true, ParameterSetName = 'Script')] [scriptblock] $ScriptBlock, [int] $Iterations = 1, [Parameter(Mandatory = $true, ParameterSetName = 'Set')] [hashtable] $TestSet ) Process { #region Running an individual testrun if ($ScriptBlock) { [System.Collections.ArrayList]$results = @() $count = 0 while ($count -lt $Iterations) { $null = $results.Add((Measure-Command -Expression $ScriptBlock)) $count++ } $measured = $results | Measure-Object -Maximum -Minimum -Average -Sum -Property Ticks [pscustomobject]@{ PSTypeName = 'PSModuleDevelopment.Performance.TestResult' Results = $results.ToArray() Max = (New-Object System.TimeSpan($measured.Maximum)) Sum = (New-Object System.TimeSpan($measured.Sum)) Min = (New-Object System.TimeSpan($measured.Minimum)) Average = (New-Object System.TimeSpan($measured.Average)) } } #endregion Running an individual testrun #region Performing a testset if ($TestSet) { $setResult = @{ } foreach ($testName in $TestSet.Keys) { $setResult[$testName] = Measure-PSMDCommand -ScriptBlock $TestSet[$testName] -Iterations $Iterations } $fastestResult = $setResult.Values | Sort-Object Average | Select-Object -First 1 $finalResult = foreach ($setName in $setResult.Keys) { $resultItem = $setResult[$setName] [pscustomobject]@{ PSTypeName = 'PSModuleDevelopment.Performance.TestSetItem' Name = $setName Efficiency = $resultItem.Average.Ticks / $fastestResult.Average.Ticks Average = $resultItem.Average Result = $resultItem } } $finalResult | Sort-Object Efficiency } #endregion Performing a testset } } function Convert-PSMDMessage { <# .SYNOPSIS Converts a file's use of PSFramework messages to strings. .DESCRIPTION Converts a file's use of PSFramework messages to strings. .PARAMETER Path Path to the file to convert. .PARAMETER OutPath Folder in which to generate the output ps1 and psd1 file. .PARAMETER EnableException Replaces user friendly yellow warnings with bloody red exceptions of doom! Use this if you want the function to throw terminating errors you want to catch. .EXAMPLE PS C:\> Convert-PSMDMessage -Path 'C:\Scripts\logrotate.ps1' -OutPath 'C:\output' Converts all instances of writing messages in logrotate.ps1 to use strings instead. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true, Position = 0)] [PsfValidateScript('PSFramework.Validate.FSPath.File', ErrorString = 'PSFramework.Validate.FSPath.File')] [string] $Path, [Parameter(Mandatory = $true, Position = 1)] [PsfValidateScript('PSFramework.Validate.FSPath.Folder', ErrorString = 'PSFramework.Validate.FSPath.Folder')] [string] $OutPath, [switch] $EnableException ) begin { #region Utility Functions function Get-Text { [OutputType([string])] [CmdletBinding()] param ( $Value ) if (-not $Value.NestedExpressions) { return $Value.Extent.Text } $expressions = @{ } $expIndex = 0 $builder = [System.Text.StringBuilder]::new() $baseIndex = $Value.Extent.StartOffset $astIndex = 0 foreach ($nestedExpression in $Value.NestedExpressions) { $null = $builder.Append($Value.Extent.Text.SubString($astIndex, ($nestedExpression.Extent.StartOffset - $baseIndex - $astIndex)).Replace("{", "{{").Replace('}', '}}')) $astIndex = $nestedExpression.Extent.EndOffset - $baseIndex if ($expressions.ContainsKey($nestedExpression.Extent.Text)) { $effectiveIndex = $expressions[$nestedExpression.Extent.Text] } else { $expressions[$nestedExpression.Extent.Text] = $expIndex $effectiveIndex = $expIndex $expIndex++ } $null = $builder.Append("{$effectiveIndex}") } $null = $builder.Append($Value.Extent.Text.SubString($astIndex).Replace("{", "{{").Replace('}', '}}')) $builder.ToString() } function Get-Insert { [OutputType([string])] [CmdletBinding()] param ( $Value ) if (-not $Value.NestedExpressions) { return "" } $processed = @{ } $elements = foreach ($nestedExpression in $Value.NestedExpressions) { if ($processed[$nestedExpression.Extent.Text]) { continue } else { $processed[$nestedExpression.Extent.Text] = $true } if ($nestedExpression -is [System.Management.Automation.Language.SubExpressionAst]) { if ( ($nestedExpression.SubExpression.Statements.Count -eq 1) -and ($nestedExpression.SubExpression.Statements[0].PipelineElements.Count -eq 1) -and ($nestedExpression.SubExpression.Statements[0].PipelineElements[0].Expression -is [System.Management.Automation.Language.MemberExpressionAst]) ) { $nestedExpression.SubExpression.Extent.Text } else { $nestedExpression.Extent.Text.SubString(1) } } else { $nestedExpression.Extent.Text } } $elements -join ", " } #endregion Utility Functions $parameterMapping = @{ 'Message' = 'String' 'Action' = 'ActionString' } $insertMapping = @{ 'String' = '-StringValues' 'Action' = '-ActionStringValues' } } process { $ast = (Read-PSMDScript -Path $Path).Ast #region Parse Input $functionName = (Get-Item $Path).BaseName $commandAsts = $ast.FindAll({ if ($args[0] -isnot [System.Management.Automation.Language.CommandAst]) { return $false } if ($args[0].CommandElements[0].Value -notmatch '^Invoke-PSFProtectedCommand$|^Write-PSFMessage$|^Stop-PSFFunction$|^Test-PSFShouldProcess$') { return $false } if (-not ($args[0].CommandElements.ParameterName -match '^Message$|^Action$')) { return $false } $true }, $true) if (-not $commandAsts) { Write-PSFMessage -Level Host -String 'Convert-PSMDMessage.Parameter.NonAffected' -StringValues $Path return } #endregion Parse Input #region Build Replacements table $currentCount = 1 $replacements = foreach ($command in $commandAsts) { $parameter = $command.CommandElements | Where-Object ParameterName -in 'Message', 'Action' $paramIndex = $command.CommandElements.IndexOf($parameter) $parameterValue = $command.CommandElements[$paramIndex + 1] [PSCustomObject]@{ OriginalText = $parameterValue.Value Text = Get-Text -Value $parameterValue Inserts = Get-Insert -Value $parameterValue String = "$($functionName).Message$($currentCount)" StartOffset = $parameter.Extent.StartOffset EndOffset = $parameterValue.Extent.EndOffset OldParameterName = $parameter.ParameterName NewParameterName = $parameterMapping[$parameter.ParameterName] Parameter = $parameter ParameterValue = $parameterValue } $currentCount++ } #endregion Build Replacements table #region Calculate new text body $fileText = [System.IO.File]::ReadAllText((Resolve-PSFPath -Path $Path)) $builder = [System.Text.StringBuilder]::new() $index = 0 foreach ($replacement in $replacements) { $null = $builder.Append($fileText.Substring($index, ($replacement.StartOffset - $index))) $null = $builder.Append("-$($replacement.NewParameterName) '$($replacement.String)'") if ($replacement.Inserts) { $null = $builder.Append(" $($insertMapping[$replacement.NewParameterName]) $($replacement.Inserts)") } $index = $replacement.EndOffset } $null = $builder.Append($fileText.Substring($index)) $newDefinition = $builder.ToString() $testResult = Read-PSMDScript -ScriptCode ([Scriptblock]::create($newDefinition)) if ($testResult.Errors) { Stop-PSFFunction -String 'Convert-PSMDMessage.SyntaxError' -StringValues $Path -Target $Path -EnableException $EnableException return } #endregion Calculate new text body $resolvedOutPath = Resolve-PSFPath -Path $OutPath $encoding = [System.Text.UTF8Encoding]::new($true) $filePath = Join-Path -Path $resolvedOutPath -ChildPath "$functionName.ps1" [System.IO.File]::WriteAllText($filePath, $newDefinition, $encoding) $stringsPath = Join-Path -Path $resolvedOutPath -ChildPath "$functionName.psd1" $stringsText = @" @{ $($replacements | Format-String "`t'{0}' = {1} # {2}" -Property String, Text, Inserts | Join-String -Separator "`n") } "@ [System.IO.File]::WriteAllText($stringsPath, $stringsText, $encoding) } } function Export-PSMDString { <# .SYNOPSIS Parses a module that uses the PSFramework localization feature for strings and their value. .DESCRIPTION Parses a module that uses the PSFramework localization feature for strings and their value. This command can be used to generate and update the language files used by the module. It is also used in automatic tests, ensuring no abandoned string has been left behind and no key is unused. .PARAMETER ModuleRoot The root of the module to process. Must be the root folder where the psd1 file is stored in. .EXAMPLE PS C:\> Export-PSMDString -ModuleRoot 'C:\Code\Github\MyModuleProject\MyModule' Generates the strings data for the MyModule module. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('ModuleBase')] [string] $ModuleRoot ) process { #region Find Language Files : $languageFiles $languageFiles = @{ } $languageFolders = Get-ChildItem -Path $ModuleRoot -Directory | Where-Object Name -match '^\w\w-\w\w$' foreach ($languageFolder in $languageFolders) { $languageFiles[$languageFolder.Name] = @{ } foreach ($file in (Get-ChildItem -Path $languageFolder.FullName -Filter *.psd1)) { $languageFiles[$languageFolder.Name] += Import-PSFPowerShellDataFile -Path $file.FullName } } #endregion Find Language Files : $languageFiles #region Find Keys : $foundKeys $foundKeys = foreach ($file in (Get-ChildItem -Path $ModuleRoot -Recurse | Where-Object Extension -match '^\.ps1$|^\.psm1$')) { $ast = (Read-PSMDScript -Path $file.FullName).Ast #region Command Parameters $commandAsts = $ast.FindAll({ if ($args[0] -isnot [System.Management.Automation.Language.CommandAst]) { return $false } if ($args[0].CommandElements[0].Value -notmatch '^Invoke-PSFProtectedCommand$|^Write-PSFMessage$|^Stop-PSFFunction$|^Test-PSFShouldProcess$|^Get-PSFLocalizedString$') { return $false } if (-not ($args[0].CommandElements.ParameterName -match '^String$|^ActionString$|^Name$')) { return $false } $true }, $true) foreach ($commandAst in $commandAsts) { $stringParam = $commandAst.CommandElements | Where-Object ParameterName -match '^String$|^ActionString$|^Name$' $stringParamValue = $commandAst.CommandElements[($commandAst.CommandElements.IndexOf($stringParam) + 1)].Value $stringValueParam = $commandAst.CommandElements | Where-Object ParameterName -match '^StringValues$|^ActionStringValues$|^Name$' if ($stringValueParam) { $stringValueParamValue = $commandAst.CommandElements[($commandAst.CommandElements.IndexOf($stringValueParam) + 1)].Extent.Text } else { $stringValueParamValue = '' } [PSCustomObject]@{ PSTypeName = 'PSModuleDevelopment.String.ParsedItem' File = $file.FullName Line = $commandAst.Extent.StartLineNumber CommandName = $commandAst.CommandElements[0].Value String = $stringParamValue StringValues = $stringValueParamValue } } #endregion Command Parameters #region Splatted Variables $splattedVariables = $ast.FindAll({ if ($args[0] -isnot [System.Management.Automation.Language.VariableExpressionAst]) { return $false } if (-not ($args[0].Splatted -eq $true)) { return $false } try { if ($args[0].Parent.CommandElements[0].Value -notmatch '^Invoke-PSFProtectedCommand$|^Write-PSFMessage$|^Stop-PSFFunction$|^Test-PSFShouldProcess$') { return $false } } catch { return $false } $true }, $true) foreach ($splattedVariable in $splattedVariables) { $splatParamName = $splattedVariable.VariablePath.UserPath $splatAssignmentAsts = $ast.FindAll({ if ($args[0] -isnot [System.Management.Automation.Language.AssignmentStatementAst]) { return $false } if ($args[0].Left.VariablePath.userPath -ne $splatParamName) { return $false } if ($args[0].Operator -ne 'Equals') { return $false } if ($args[0].Right.Expression -isnot [System.Management.Automation.Language.HashtableAst]) { return $false } $keys = $args[0].Right.Expression.KeyValuePairs.Item1.Value if (($keys -notcontains 'String') -and ($keys -notcontains 'ActionString')) { return $false } $true }, $true) foreach ($splatAssignmentAst in $splatAssignmentAsts) { $splatHashTable = $splatAssignmentAst.Right.Expression $splatParam = $splathashTable.KeyValuePairs | Where-Object Item1 -in 'String', 'ActionString' $splatValueParam = $splathashTable.KeyValuePairs | Where-Object Item1 -in 'StringValues', 'ActionStringValues' if ($splatValueParam) { $splatValueParamValue = $splatValueParam.Item2.Extent.Text } else { $splatValueParamValue = '' } [PSCustomObject]@{ PSTypeName = 'PSModuleDevelopment.String.ParsedItem' File = $file.FullName Line = $splatHashTable.Extent.StartLineNumber CommandName = $splattedVariable.Parent.CommandElements[0].Value String = $splatParam.Item2.Extent.Text.Trim("'").Trim('"') StringValues = $splatValueParamValue } } } #endregion Splatted Variables #region Attributes $validateAsts = $ast.FindAll({ if ($args[0] -isnot [System.Management.Automation.Language.AttributeAst]) { return $false } if ($args[0].TypeName -notmatch '^PsfValidateScript$|^PsfValidatePattern$') { return $false } if (-not ($args[0].NamedArguments.ArgumentName -eq 'ErrorString')) { return $false } $true }, $true) foreach ($validateAst in $validateAsts) { [PSCustomObject]@{ PSTypeName = 'PSModuleDevelopment.String.ParsedItem' File = $file.FullName Line = $commandAst.Extent.StartLineNumber CommandName = '[{0}]' -f $validateAst.TypeName String = (($validateAst.NamedArguments | Where-Object ArgumentName -eq 'ErrorString').Argument.Value -split "\.", 2)[1] # The first element is the module element StringValues = '<user input>, <validation item>' } } #endregion Attributes } #endregion Find Keys : $foundKeys #region Report Findings $totalResults = foreach ($languageFile in $languageFiles.Keys) { #region Phase 1: Matching parsed strings to language file $results = @{ } foreach ($foundKey in $foundKeys) { if ($results[$foundKey.String]) { $results[$foundKey.String].Entries += $foundKey continue } $results[$foundKey.String] = [PSCustomObject] @{ PSTypeName = 'PSmoduleDevelopment.String.LanguageFinding' Language = $languageFile Surplus = $false String = $foundKey.String StringValues = $foundKey.StringValues Text = $languageFiles[$languageFile][$foundKey.String] Line = "'{0}' = '{1}' # {2}" -f $foundKey.String, $languageFiles[$languageFile][$foundKey.String], $foundKey.StringValues Entries = @($foundKey) } } $results.Values #endregion Phase 1: Matching parsed strings to language file #region Phase 2: Finding unneeded strings foreach ($key in $languageFiles[$languageFile].Keys) { if ($key -notin $foundKeys.String) { [PSCustomObject] @{ PSTypeName = 'PSmoduleDevelopment.String.LanguageFinding' Language = $languageFile Surplus = $true String = $key StringValues = '' Text = $languageFiles[$languageFile][$key] Line = '' Entries = @() } } } #endregion Phase 2: Finding unneeded strings } $totalResults | Sort-Object String #endregion Report Findings } } function Format-PSMDParameter { <# .SYNOPSIS Formats the parameter block on commands. .DESCRIPTION Formats the parameter block on commands. This function will convert legacy functions that have their parameters straight behind their command name. It also fixes missing CmdletBinding attributes. Nested commands will also be affected. .PARAMETER FullName The file to process .PARAMETER DisableCache By default, this command caches the results of its execution in the PSFramework result cache. This information can then be retrieved for the last command to do so by running Get-PSFResultCache. Setting this switch disables the caching of data in the cache. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .EXAMPLE PS C:\> Get-ChildItem .\functions\*\*.ps1 | Set-PSMDCmdletBinding Updates all commands in the module to have a cmdletbinding attribute. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')] [CmdletBinding(SupportsShouldProcess = $true)] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [string[]] $FullName, [switch] $DisableCache ) begin { #region Utility functions function Invoke-AstWalk { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')] [CmdletBinding()] param ( $Ast, [string[]] $Command, [string[]] $Name, [string] $NewName, [bool] $IsCommand, [bool] $NoAlias ) #Write-PSFMessage -Level Host -Message "Processing $($Ast.Extent.StartLineNumber) | $($Ast.Extent.File) | $($Ast.GetType().FullName)" $typeName = $Ast.GetType().FullName switch ($typeName) { "System.Management.Automation.Language.FunctionDefinitionAst" { #region Has no param block if ($null -eq $Ast.Body.ParamBlock) { $baseIndent = $Ast.Extent.Text.Split("`n")[0] -replace "^(\s{0,}).*", '$1' $indent = $baseIndent + "`t" # Kill explicit parameter section behind name $startIndex = "function ".Length + $Ast.Name.Length $endIndex = $Ast.Extent.Text.IndexOf("{") Add-FileReplacement -Path $ast.Extent.File -Start ($Ast.Extent.StartOffset + $startIndex) -Length ($endIndex - $startIndex) -NewContent "`n" $baseParam = @" $($indent)[CmdletBinding()] $($indent)param ( {0} $($indent)) "@ $parameters = @() $paramIndent = $indent + "`t" foreach ($parameter in $Ast.Parameters) { $defaultValue = "" if ($parameter.DefaultValue) { $defaultValue = " = $($parameter.DefaultValue.Extent.Text)" } $values = @() foreach ($attribute in $parameter.Attributes) { $values += "$($paramIndent)$($attribute.Extent.Text)" } $values += "$($paramIndent)$($parameter.Name.Extent.Text)$($defaultValue)" $parameters += $values -join "`n" } $baseParam = $baseParam -f ($parameters -join ",`n`n") Add-FileReplacement -Path $ast.Extent.File -Start $Ast.Body.Extent.StartOffset -Length 1 -NewContent "{`n$($baseParam)" } #endregion Has no param block #region Has a param block, but no cmdletbinding if (($null -ne $Ast.Body.ParamBlock) -and (-not ($Ast.Body.ParamBlock.Attributes | Where-Object TypeName -Like "CmdletBinding"))) { $text = [System.IO.File]::ReadAllText($Ast.Extent.File) $index = $Ast.Body.ParamBlock.Extent.StartOffset while (($index -gt 0) -and ($text.Substring($index, 1) -ne "`n")) { $index = $index - 1 } $indentIndex = $index + 1 $indent = $text.Substring($indentIndex, ($Ast.Body.ParamBlock.Extent.StartOffset - $indentIndex)) Add-FileReplacement -Path $Ast.Body.ParamBlock.Extent.File -Start $indentIndex -Length ($Ast.Body.ParamBlock.Extent.StartOffset - $indentIndex) -NewContent "$($indent)[CmdletBinding()]`n$($indent)" } #endregion Has a param block, but no cmdletbinding Invoke-AstWalk -Ast $Ast.Body -Command $Command -Name $Name -NewName $NewName -IsCommand $false } default { foreach ($property in $Ast.PSObject.Properties) { if ($property.Name -eq "Parent") { continue } if ($null -eq $property.Value) { continue } if (Get-Member -InputObject $property.Value -Name GetEnumerator -MemberType Method) { foreach ($item in $property.Value) { if ($item.PSObject.TypeNames -contains "System.Management.Automation.Language.Ast") { Invoke-AstWalk -Ast $item -Command $Command -Name $Name -NewName $NewName -IsCommand $IsCommand } } continue } if ($property.Value.PSObject.TypeNames -contains "System.Management.Automation.Language.Ast") { Invoke-AstWalk -Ast $property.Value -Command $Command -Name $Name -NewName $NewName -IsCommand $IsCommand } } } } } function Add-FileReplacement { [CmdletBinding()] param ( [string] $Path, [int] $Start, [int] $Length, [string] $NewContent ) Write-PSFMessage -Level Verbose -Message "Change Submitted: $Path | $Start | $Length | $NewContent" -Tag 'update', 'change', 'file' if (-not $globalFunctionHash.ContainsKey($Path)) { $globalFunctionHash[$Path] = @() } $globalFunctionHash[$Path] += New-Object PSObject -Property @{ Content = $NewContent Start = $Start Length = $Length } } function Apply-FileReplacement { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "")] [CmdletBinding()] param ( ) foreach ($key in $globalFunctionHash.Keys) { $value = $globalFunctionHash[$key] | Sort-Object Start $content = [System.IO.File]::ReadAllText($key) $newString = "" $currentIndex = 0 foreach ($item in $value) { $newString += $content.SubString($currentIndex, ($item.Start - $currentIndex)) $newString += $item.Content $currentIndex = $item.Start + $item.Length } $newString += $content.SubString($currentIndex) [System.IO.File]::WriteAllText($key, $newString) #$newString } } function Write-Issue { [CmdletBinding()] param ( $Extent, $Data, [string] $Type ) New-Object PSObject -Property @{ Type = $Type Data = $Data File = $Extent.File StartLine = $Extent.StartLineNumber Text = $Extent.Text } } #endregion Utility functions } process { foreach ($path in $FullName) { $globalFunctionHash = @{ } $tokens = $null $parsingError = $null $ast = [System.Management.Automation.Language.Parser]::ParseFile($path, [ref]$tokens, [ref]$parsingError) Write-PSFMessage -Level VeryVerbose -Message "Ensuring Cmdletbinding for all functions in $path" -Tag 'start' -Target $Name $issues += Invoke-AstWalk -Ast $ast -Command $Command -Name $Name -NewName $NewName -IsCommand $false Set-PSFResultCache -InputObject $issues -DisableCache $DisableCache if ($PSCmdlet.ShouldProcess($path, "Set CmdletBinding attribute")) { Apply-FileReplacement } $issues } } } function Get-PSMDFileCommand { <# .SYNOPSIS Parses a scriptfile and returns the contained/used commands. .DESCRIPTION Parses a scriptfile and returns the contained/used commands. Use this to determine, what command resources are being used. .PARAMETER Path The path to the scriptfile to parse. .PARAMETER EnableException Replaces user friendly yellow warnings with bloody red exceptions of doom! Use this if you want the function to throw terminating errors you want to catch. .EXAMPLE PS C:\> Get-PSMDFileCommand -Path './task_usersync.ps1' Parses the scriptfile task_usersync.ps1 for commands used. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [PsfValidateScript('PSModuleDevelopment.Validate.Path', ErrorString = 'PSModuleDevelopment.Validate.Path')] [string[]] $Path, [switch] $EnableException ) process { foreach ($pathItem in $Path) { # Skip Folders if (-not (Test-Path -Path $pathItem -PathType Leaf)) { continue } $parsedCode = Read-PSMDScript -Path $pathItem if ($parsedCode.Errors) { Stop-PSFFunction -String 'Get-PSMDFileCommand.SyntaxError' -StringValues $pathItem -EnableException $EnableException -Continue } $results = @{ } $commands = $parsedCode.Ast.FindAll({ $args[0] -is [System.Management.Automation.Language.CommandAst] }, $true) $internalCommands = $parsedCode.Ast.FindAll({ $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $true).Name foreach ($command in $commands) { if (-not $results[$command.CommandElements[0].Value]) { $commandInfo = Get-Command $command.CommandElements[0].Value -ErrorAction Ignore $module = $commandInfo.Module if (-not $module) { $module = $commandInfo.PSSnapin } $results[$command.CommandElements[0].Value] = [pscustomobject]@{ PSTypeName = 'PSModuleDevelopment.File.Command' File = Get-Item $pathItem Name = $command.CommandElements[0].Value Parameters = @{ } Count = 0 AstObjects = @() CommandInfo = $commandInfo Module = $module Internal = $command.CommandElements[0].Value -in $internalCommands Path = $pathItem } } $object = $results[$command.CommandElements[0].Value] $object.Count = $object.Count + 1 $object.AstObjects += $command foreach ($parameter in $command.CommandElements.Where{ $_ -is [System.Management.Automation.Language.CommandParameterAst] }) { if (-not $object.Parameters[$parameter.ParameterName]) { $object.Parameters[$parameter.ParameterName] = 1 } else { $object.Parameters[$parameter.ParameterName] = $object.Parameters[$parameter.ParameterName] + 1 } } } $results.Values } } } function Read-PSMDScript { <# .SYNOPSIS Parse the content of a script .DESCRIPTION Uses the powershell parser to parse the content of a script or scriptfile. .PARAMETER ScriptCode The scriptblock to parse. .PARAMETER Path Path to the scriptfile to parse. Silently ignores folder objects. .EXAMPLE PS C:\> Read-PSMDScript -ScriptCode $ScriptCode Parses the code in $ScriptCode .EXAMPLE PS C:\> Get-ChildItem | Read-PSMDScript Parses all script files in the current directory #> [Alias('parse')] [CmdletBinding()] param ( [Parameter(Position = 0, ParameterSetName = 'Script', Mandatory = $true)] [System.Management.Automation.ScriptBlock] $ScriptCode, [Parameter(Mandatory = $true, ParameterSetName = 'File', ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('FullName')] [string[]] $Path ) begin { Write-PSFMessage -Level InternalComment -Message "Bound parameters: $($PSBoundParameters.Keys -join ", ")" -Tag 'debug', 'start', 'param' } process { foreach ($file in $Path) { Write-PSFMessage -Level Verbose -Message "Processing $file" -Target $file $item = Get-Item $file if ($item.PSIsContainer) { Write-PSFMessage -Level Verbose -Message "is folder, skipping $file" -Target $file continue } $tokens = $null $errors = $null $ast = [System.Management.Automation.Language.Parser]::ParseFile($item.FullName, [ref]$tokens, [ref]$errors) [pscustomobject]@{ PSTypeName = 'PSModuleDevelopment.Meta.ParseResult' Ast = $ast Tokens = $tokens Errors = $errors File = $item.FullName } } if ($ScriptCode) { $tokens = $null $errors = $null $ast = [System.Management.Automation.Language.Parser]::ParseInput($ScriptCode, [ref]$tokens, [ref]$errors) [pscustomobject]@{ PSTypeName = 'PSModuleDevelopment.Meta.ParseResult' Ast = $ast Tokens = $tokens Errors = $errors Source = $ScriptCode } } } } function Rename-PSMDParameter { <# .SYNOPSIS Renames a parameter of a function. .DESCRIPTION This command is designed to rename the parameter of a function within an entire module. By default it will add an alias for the previous command name. In order for this to work you need to consider to have the command / module imported. Hint: Import the psm1 file for best results. It will then search all files in the specified path (hint: Specify module root for best results), and update all psm1/ps1 files. At the same time it will force all commands to call the parameter by its new standard, even if they previously used an alias for the parameter. While this command was designed to work with a module, it is not restricted to that: You can load a standalone function and specify a path with loose script files for the same effect. Note: You can also use this to update your scripts, after a foreign module introduced a breaking change by renaming a parameter. In this case, import the foreign module to see the function, but point it at the base path of your scripts to update. The loaded function is only used for alias/parameter alias resolution .PARAMETER Path The path to the root folder where all the files are stored. It will search the folder recursively and ignore hidden files & folders. .PARAMETER Command The name of the function, whose parameter should be changed. Most be loaded into the current runtime. .PARAMETER Name The name of the parameter to change. .PARAMETER NewName The new name for the parameter. Do not specify "-" or the "$" symbol .PARAMETER NoAlias Avoid creating an alias for the old parameter name. This may cause a breaking change! .PARAMETER WhatIf Prevents the command from updating the files. Instead it will return the strings of all its changes. .PARAMETER EnableException Replaces user friendly yellow warnings with bloody red exceptions of doom! Use this if you want the function to throw terminating errors you want to catch. .PARAMETER DisableCache By default, this command caches the results of its execution in the PSFramework result cache. This information can then be retrieved for the last command to do so by running Get-PSFResultCache. Setting this switch disables the caching of data in the cache. .EXAMPLE PS C:\> Rename-PSMDParameter -Path 'C:\Scripts\Modules\MyModule' -Command 'Get-Test' -Name 'Foo' -NewName 'Bar' Renames the parameter 'Foo' of the command 'Get-Test' to 'Bar' for all scripts stored in 'C:\Scripts\Modules\MyModule' #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSupportsShouldProcess", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseOutputTypeCorrectly", "")] [CmdletBinding()] Param ( [Parameter(Mandatory = $true)] [string] $Path, [Parameter(Mandatory = $true)] [string[]] $Command, [Parameter(Mandatory = $true)] [string] $Name, [Parameter(Mandatory = $true)] [string] $NewName, [switch] $NoAlias, [switch] $WhatIf, [switch] $EnableException, [switch] $DisableCache ) # Global Store for pending file updates # Exempt from Scope Boundary violation rule, since only accessed using dedicated helper function $globalFunctionHash = @{ } #region Helper Functions function Invoke-AstWalk { [CmdletBinding()] Param ( $Ast, [string[]] $Command, [string[]] $Name, [string] $NewName, [bool] $IsCommand, [bool] $NoAlias ) #Write-PSFMessage -Level Host -Message "Processing $($Ast.Extent.StartLineNumber) | $($Ast.Extent.File) | $($Ast.GetType().FullName)" $typeName = $Ast.GetType().FullName switch ($typeName) { "System.Management.Automation.Language.CommandAst" { Write-PSFMessage -Level Verbose -Message "Line $($Ast.Extent.StartLineNumber): Processing Command Ast: <c='em'>$($Ast.Extent.ToString())</c>" $commandName = $Ast.CommandElements[0].Value $resolvedCommand = $commandName if (Test-Path function:\$commandName) { $resolvedCommand = (Get-Item function:\$commandName).Name } if (Test-Path alias:\$commandName) { $resolvedCommand = (Get-Item alias:\$commandName).ResolvedCommand.Name } if ($resolvedCommand -in $Command) { $parameters = $Ast.CommandElements | Where-Object { $_.GetType().FullName -eq "System.Management.Automation.Language.CommandParameterAst" } foreach ($parameter in $parameters) { if ($parameter.ParameterName -in $Name) { Write-PSFMessage -Level SomewhatVerbose -Message "Found parameter: <c='em'>$($parameter.ParameterName)</c>" Update-CommandParameter -Ast $parameter -NewName $NewName } } $splatted = $Ast.CommandElements | Where-Object Splatted if ($splatted) { foreach ($splat in $splatted) { Write-PSFMessage -Level Warning -FunctionName Rename-PSMDParameter -Message "Splat detected! Manually verify $($splat.Extent.Text) at line $($splat.Extent.StartLineNumber) in file $($splat.Extent.File)" -Tag 'splat','fail','manual' Write-Issue -Extent $splat.Extent -Data $Ast -Type "SplattedParameter" } } } foreach ($element in $Ast.CommandElements) { if ($element.GetType().FullName -ne "System.Management.Automation.Language.CommandParameterAst") { Invoke-AstWalk -Ast $element -Command $Command -Name $Name -NewName $NewName -IsCommand $IsCommand -NoAlias $NoAlias } } } "System.Management.Automation.Language.FunctionDefinitionAst" { if ($Ast.Name -In $Command) { foreach ($parameter in $Ast.Body.ParamBlock.Parameters) { if ($Name[0] -ne $parameter.Name.VariablePath.UserPath) { continue } $stringExtent = $parameter.Extent.ToString() $lines = $stringExtent.Split("`n") $multiLine = $lines -gt 1 $indent = 0 $indentStyle = "`t" if ($multiLine) { if ($lines[1][0] -eq " ") { $indentStyle = " " } $indent = $lines[1].Length - $lines[1].Trim().Length } $aliases = @() foreach ($attribute in $parameter.Attributes) { if ($attribute.TypeName.FullName -eq "Alias") { $aliases += $attribute } } $aliasNames = $aliases.PositionalArguments.Value if ($aliasNames -contains $NewName) { $aliasNames = $aliasNames | Where-Object { $_ -ne $NewName } } if (-not $NoAlias) { $aliasNames += $Name } $aliasNames = $aliasNames | Select-Object -Unique | Sort-Object if ($aliasNames) { if ($aliases) { $newAlias = "[Alias($("'" + ($aliasNames -join "','")+ "'"))]" Add-FileReplacement -Path $aliases[0].Extent.File -Start $aliases[0].Extent.StartOffset -Length ($aliases[0].Extent.EndOffset - $aliases[0].Extent.StartOffset) -NewContent $newAlias Add-FileReplacement -Path $parameter.Name.Extent.File -Start $parameter.Name.Extent.StartOffset -Length ($parameter.Name.Extent.EndOffset - $parameter.Name.Extent.StartOffset) -NewContent "`$$NewName" } else { if ($multiLine) { $newAliasAndName = "[Alias($("'" + ($aliasNames -join "','") + "'"))]`n$($indentStyle * $indent)`$$NewName" } else { $newAliasAndName = "[Alias($("'" + ($aliasNames -join "','") + "'"))]`$$NewName" } Add-FileReplacement -Path $parameter.Name.Extent.File -Start $parameter.Name.Extent.StartOffset -Length ($parameter.Name.Extent.EndOffset - $parameter.Name.Extent.StartOffset) -NewContent $newAliasAndName } } else { Add-FileReplacement -Path $parameter.Name.Extent.File -Start $parameter.Name.Extent.StartOffset -Length ($parameter.Name.Extent.EndOffset - $parameter.Name.Extent.StartOffset) -NewContent "`$$NewName" } } if ($Ast.Body.DynamicParamBlock) { Invoke-AstWalk -Ast $Ast.Body.DynamicParamBlock -Command $Command -Name $Name -NewName $NewName -IsCommand $true -NoAlias $NoAlias } if ($Ast.Body.BeginBlock) { Invoke-AstWalk -Ast $Ast.Body.BeginBlock -Command $Command -Name $Name -NewName $NewName -IsCommand $true -NoAlias $NoAlias } if ($Ast.Body.ProcessBlock) { Invoke-AstWalk -Ast $Ast.Body.ProcessBlock -Command $Command -Name $Name -NewName $NewName -IsCommand $true -NoAlias $NoAlias } if ($Ast.Body.EndBlock) { Invoke-AstWalk -Ast $Ast.Body.EndBlock -Command $Command -Name $Name -NewName $NewName -IsCommand $true -NoAlias $NoAlias } Update-CommandParameterHelp -FunctionAst $Ast -ParameterName $Name[0] -NewName $NewName } else { Invoke-AstWalk -Ast $Ast.Body -Command $Command -Name $Name -NewName $NewName -IsCommand $false -NoAlias $NoAlias } } "System.Management.Automation.Language.VariableExpressionAst" { if ($IsCommand -and ($Ast.VariablePath.UserPath -eq $Name)) { Add-FileReplacement -Path $Ast.Extent.File -Start $Ast.Extent.StartOffset -Length ($Ast.Extent.EndOffset - $Ast.Extent.StartOffset) -NewContent "`$$NewName" } } "System.Management.Automation.Language.IfStatementAst" { foreach ($clause in $Ast.Clauses) { Invoke-AstWalk -Ast $clause.Item1 -Command $Command -Name $Name -NewName $NewName -IsCommand $IsCommand -NoAlias $NoAlias Invoke-AstWalk -Ast $clause.Item2 -Command $Command -Name $Name -NewName $NewName -IsCommand $IsCommand -NoAlias $NoAlias } if ($Ast.ElseClause) { Invoke-AstWalk -Ast $Ast.ElseClause -Command $Command -Name $Name -NewName $NewName -IsCommand $IsCommand -NoAlias $NoAlias } } default { foreach ($property in $Ast.PSObject.Properties) { if ($property.Name -eq "Parent") { continue } if ($null -eq $property.Value) { continue } if (Get-Member -InputObject $property.Value -Name GetEnumerator -MemberType Method) { foreach ($item in $property.Value) { if ($item.PSObject.TypeNames -contains "System.Management.Automation.Language.Ast") { Invoke-AstWalk -Ast $item -Command $Command -Name $Name -NewName $NewName -IsCommand $IsCommand -NoAlias $NoAlias } } continue } if ($property.Value.PSObject.TypeNames -contains "System.Management.Automation.Language.Ast") { Invoke-AstWalk -Ast $property.Value -Command $Command -Name $Name -NewName $NewName -IsCommand $IsCommand -NoAlias $NoAlias } } } } } function Update-CommandParameter { [CmdletBinding()] Param ( [System.Management.Automation.Language.CommandParameterAst] $Ast, [string] $NewName ) $name = $NewName if ($name -notlike "-*") { $name = "-$name" } $length = $Ast.Extent.EndOffset - $Ast.Extent.StartOffset if ($null -ne $Ast.Argument) { $length = $Ast.Argument.Extent.StartOffset - $Ast.Extent.StartOffset - 1 } Add-FileReplacement -Path $Ast.Extent.File -Start $Ast.Extent.StartOffset -Length $length -NewContent $name } function Update-CommandParameterHelp { [CmdletBinding()] Param ( [System.Management.Automation.Language.FunctionDefinitionAst] $FunctionAst, [string] $ParameterName, [string] $NewName ) function Get-StartIndex { [CmdletBinding()] Param ( [System.Management.Automation.Language.FunctionDefinitionAst] $FunctionAst, [string] $ParameterName, [int] $HelpEnd ) if ($HelpEnd -lt 1) { return -1 } $index = -1 $offset = 0 while ($FunctionAst.Extent.Text.SubString(0, $HelpEnd).IndexOf(".PARAMETER $ParameterName", $offset, [System.StringComparison]::InvariantCultureIgnoreCase) -ne -1) { $tempIndex = $FunctionAst.Extent.Text.SubString(0, $HelpEnd).IndexOf(".PARAMETER $ParameterName", $offset, [System.StringComparison]::InvariantCultureIgnoreCase) $endOfLineIndex = $FunctionAst.Extent.Text.SubString(0, $HelpEnd).IndexOf("`n", $tempIndex, [System.StringComparison]::InvariantCultureIgnoreCase) if ($FunctionAst.Extent.Text.SubString($tempIndex, ($endOfLineIndex - $tempIndex)).Trim() -eq ".PARAMETER $ParameterName") { return $tempIndex } $offset = $endOfLineIndex } return $index } $startIndex = $FunctionAst.Extent.StartOffset $endIndex = $FunctionAst.Body.ParamBlock.Extent.StartOffset foreach ($attribute in $FunctionAst.Body.ParamBlock.Attributes) { if ($attribute.Extent.StartOffset -lt $endIndex) { $endIndex = $attribute.Extent.StartOffset } } $index1 = Get-StartIndex -FunctionAst $FunctionAst -ParameterName $ParameterName -HelpEnd ($endIndex - $startIndex) if ($index1 -eq -1) { Write-PSFMessage -Level Warning -Message "Could not find Comment Based Help for parameter '$ParameterName' of command '$($FunctionAst.Name)' in '$($FunctionAst.Extent.File)'" -Tag 'cbh', 'fail' -FunctionName Rename-PSMDParameter Write-Issue -Extent $FunctionAst.Extent -Type "ParameterCBHNotFound" -Data "Parameter Help not found" return } $index2 = $FunctionAst.Extent.Text.SubString(0, ($endIndex - $startIndex)).IndexOf("$ParameterName", $index1, [System.StringComparison]::InvariantCultureIgnoreCase) Add-FileReplacement -Path $FunctionAst.Extent.File -Start ($index2 + $startIndex) -Length $ParameterName.Length -NewContent $NewName } function Add-FileReplacement { [CmdletBinding()] Param ( [string] $Path, [int] $Start, [int] $Length, [string] $NewContent ) Write-PSFMessage -Level Verbose -Message "Change Submitted: $Path | $Start | $Length | $NewContent" -Tag 'update','change','file' if (-not $globalFunctionHash.ContainsKey($Path)) { $globalFunctionHash[$Path] = @() } $globalFunctionHash[$Path] += New-Object PSObject -Property @{ Content = $NewContent Start = $Start Length = $Length } } function Apply-FileReplacement { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "")] [CmdletBinding()] Param ( [bool] $WhatIf ) foreach ($key in $globalFunctionHash.Keys) { $value = $globalFunctionHash[$key] | Sort-Object Start $content = [System.IO.File]::ReadAllText($key) $newString = "" $currentIndex = 0 foreach ($item in $value) { $newString += $content.SubString($currentIndex, ($item.Start - $currentIndex)) $newString += $item.Content $currentIndex = $item.Start + $item.Length } $newString += $content.SubString($currentIndex) if ($WhatIf) { $newString } else { [System.IO.File]::WriteAllText($key, $newString) } } } function Write-Issue { [CmdletBinding()] Param ( $Extent, $Data, [string] $Type ) New-Object PSObject -Property @{ Type = $Type Data = $Data File = $Extent.File StartLine = $Extent.StartLineNumber Text = $Extent.Text } } #endregion Helper Functions foreach ($item in $Command) { try { $com = Get-Item function:\$item -ErrorAction Stop } catch { Stop-PSFFunction -Message "Could not find command, please import the module using the psm1 file before starting a refactor" -EnableException $EnableException -Category ObjectNotFound -ErrorRecord $_ -OverrideExceptionMessage -Tag "fail", "input" return } } $files = Get-ChildItem -Path $Path -Recurse | Where-Object Extension -Match "\.ps1|\.psm1" $issues = @() foreach ($file in $files) { $tokens = $null $parsingError = $null $ast = [System.Management.Automation.Language.Parser]::ParseFile($file.FullName, [ref]$tokens, [ref]$parsingError) Write-PSFMessage -Level VeryVerbose -Message "Replacing <c='sub'>$Command / $Name</c> with <c='em'>$NewName</c> | Scanning $($file.FullName)" -Tag 'start' -Target $Name $issues += Invoke-AstWalk -Ast $ast -Command $Command -Name $Name -NewName $NewName -IsCommand $false -NoAlias $NoAlias } Set-PSFResultCache -InputObject $issues -DisableCache $DisableCache Apply-FileReplacement -WhatIf $WhatIf $issues } function Set-PSMDCmdletBinding { <# .SYNOPSIS Adds cmdletbinding attributes in bulk .DESCRIPTION Searches the specified file(s) for functions that ... - Do not have a cmdlet binding attribute - Do have a param block and inserts a cmdletbinding attribute for them. Will not change files where functions already have this attribute. Will also update internal functions. .PARAMETER FullName The file to process .PARAMETER DisableCache By default, this command caches the results of its execution in the PSFramework result cache. This information can then be retrieved for the last command to do so by running Get-PSFResultCache. Setting this switch disables the caching of data in the cache. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .EXAMPLE PS C:\> Get-ChildItem .\functions\*\*.ps1 | Set-PSMDCmdletBinding Updates all commands in the module to have a cmdletbinding attribute. #> [CmdletBinding(SupportsShouldProcess = $true)] Param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [string[]] $FullName, [switch] $DisableCache ) begin { #region Utility functions function Invoke-AstWalk { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')] [CmdletBinding()] Param ( $Ast, [string[]] $Command, [string[]] $Name, [string] $NewName, [bool] $IsCommand, [bool] $NoAlias ) #Write-PSFMessage -Level Host -Message "Processing $($Ast.Extent.StartLineNumber) | $($Ast.Extent.File) | $($Ast.GetType().FullName)" $typeName = $Ast.GetType().FullName switch ($typeName) { "System.Management.Automation.Language.FunctionDefinitionAst" { #region Has a param block, but no cmdletbinding if (($null -ne $Ast.Body.ParamBlock) -and (-not ($Ast.Body.ParamBlock.Attributes | Where-Object TypeName -Like "CmdletBinding"))) { $text = [System.IO.File]::ReadAllText($Ast.Extent.File) $index = $Ast.Body.ParamBlock.Extent.StartOffset while (($index -gt 0) -and ($text.Substring($index, 1) -ne "`n")) { $index = $index - 1 } $indentIndex = $index + 1 $indent = $text.Substring($indentIndex, ($Ast.Body.ParamBlock.Extent.StartOffset - $indentIndex)) Add-FileReplacement -Path $Ast.Body.ParamBlock.Extent.File -Start $indentIndex -Length ($Ast.Body.ParamBlock.Extent.StartOffset - $indentIndex) -NewContent "$($indent)[CmdletBinding()]`n$($indent)" } #endregion Has a param block, but no cmdletbinding Invoke-AstWalk -Ast $Ast.Body -Command $Command -Name $Name -NewName $NewName -IsCommand $false } default { foreach ($property in $Ast.PSObject.Properties) { if ($property.Name -eq "Parent") { continue } if ($null -eq $property.Value) { continue } if (Get-Member -InputObject $property.Value -Name GetEnumerator -MemberType Method) { foreach ($item in $property.Value) { if ($item.PSObject.TypeNames -contains "System.Management.Automation.Language.Ast") { Invoke-AstWalk -Ast $item -Command $Command -Name $Name -NewName $NewName -IsCommand $IsCommand } } continue } if ($property.Value.PSObject.TypeNames -contains "System.Management.Automation.Language.Ast") { Invoke-AstWalk -Ast $property.Value -Command $Command -Name $Name -NewName $NewName -IsCommand $IsCommand } } } } } function Add-FileReplacement { [CmdletBinding()] Param ( [string] $Path, [int] $Start, [int] $Length, [string] $NewContent ) Write-PSFMessage -Level Verbose -Message "Change Submitted: $Path | $Start | $Length | $NewContent" -Tag 'update', 'change', 'file' if (-not $globalFunctionHash.ContainsKey($Path)) { $globalFunctionHash[$Path] = @() } $globalFunctionHash[$Path] += New-Object PSObject -Property @{ Content = $NewContent Start = $Start Length = $Length } } function Apply-FileReplacement { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "")] [CmdletBinding()] Param ( ) foreach ($key in $globalFunctionHash.Keys) { $value = $globalFunctionHash[$key] | Sort-Object Start $content = [System.IO.File]::ReadAllText($key) $newString = "" $currentIndex = 0 foreach ($item in $value) { $newString += $content.SubString($currentIndex, ($item.Start - $currentIndex)) $newString += $item.Content $currentIndex = $item.Start + $item.Length } $newString += $content.SubString($currentIndex) [System.IO.File]::WriteAllText($key, $newString) #$newString } } function Write-Issue { [CmdletBinding()] Param ( $Extent, $Data, [string] $Type ) New-Object PSObject -Property @{ Type = $Type Data = $Data File = $Extent.File StartLine = $Extent.StartLineNumber Text = $Extent.Text } } #endregion Utility functions } process { foreach ($path in $FullName) { $globalFunctionHash = @{ } $tokens = $null $parsingError = $null $ast = [System.Management.Automation.Language.Parser]::ParseFile($path, [ref]$tokens, [ref]$parsingError) Write-PSFMessage -Level VeryVerbose -Message "Ensuring Cmdletbinding for all functions in $path" -Tag 'start' -Target $Name $issues += Invoke-AstWalk -Ast $ast -Command $Command -Name $Name -NewName $NewName -IsCommand $false Set-PSFResultCache -InputObject $issues -DisableCache $DisableCache if ($PSCmdlet.ShouldProcess($path, "Set CmdletBinding attribute")) { Apply-FileReplacement } $issues } } } function Set-PSMDEncoding { <# .SYNOPSIS Sets the encoding for the input file. .DESCRIPTION This command reads the input file using the default encoding interpreter. It then writes the contents as the specified enconded string back to itself. There is no inherent encoding conversion enacted, so special characters may break. This is a tool designed to reformat code files, where special characters shouldn't be used anyway. .PARAMETER Path Path to the files to be set. Silently ignores folders. .PARAMETER Encoding The encoding to set to (Defaults to "UTF8 with BOM") .PARAMETER EnableException Replaces user friendly yellow warnings with bloody red exceptions of doom! Use this if you want the function to throw terminating errors you want to catch. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .EXAMPLE PS C:\> Get-ChildItem -Recurse | Set-PSMDEncoding Converts all files in the current folder and subfolders to UTF8 #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low')] param ( [Parameter(ValueFromPipeline = $true, Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [Alias('FullName')] [string[]] $Path, [PSFEncoding] $Encoding = (Get-PSFConfigValue -FullName 'psframework.text.encoding.defaultwrite' -Fallback 'utf-8'), [switch] $EnableException ) process { foreach ($pathItem in $Path) { Write-PSFMessage -Level VeryVerbose -Message "Processing $pathItem" -Target $pathItem try { $pathResolved = Resolve-PSFPath -Path $pathItem -Provider FileSystem } catch { Stop-PSFFunction -Message " " -EnableException $EnableException -ErrorRecord $_ -Target $pathItem -Continue } foreach ($resolvedPath in $pathResolved) { if ((Get-Item $resolvedPath).PSIsContainer) { continue } Write-PSFMessage -Level Verbose -Message "Setting encoding for $resolvedPath" -Target $pathItem try { if (Test-PSFShouldProcess -PSCmdlet $PSCmdlet -Target $resolvedPath -Action "Set encoding to $($Encoding.Encoding.EncodingName)") { $text = [System.IO.File]::ReadAllText($resolvedPath) [System.IO.File]::WriteAllText($resolvedPath, $text, $Encoding) } } catch { Stop-PSFFunction -Message "Failed to access file! $resolvedPath" -EnableException $EnableException -ErrorRecord $_ -Target $pathItem -Continue } } } } } function Set-PSMDParameterHelp { <# .SYNOPSIS Sets the content of a CBH parameter help. .DESCRIPTION Sets the content of a CBH parameter help. This command will enumerate all files in the specified folder and subfolders. Then scan all files with extension .ps1 and .psm1. In each of these files it will check out function definitions, see whether the name matches, then update the help for the specified parameter if present. In order for this to work, a few rules must be respected: - It will not work with help XML, only with CBH xml - It will not work if the help block is above the function. It must be placed within. - It will not ADD a CBH, if none is present yet. If there is no help for the specified parameter, it will simply do nothing, but report the fact. .PARAMETER Path The base path where all the files are in. .PARAMETER CommandName The name of the command to update. Uses wildcard matching to match, so you can do a global update using "*" .PARAMETER ParameterName The name of the parameter to update. Must be an exact match, but is not case sensitive. .PARAMETER HelpText The text to insert. - Do not include indents. It will pick up the previous indents and reuse them - Do not include an extra line, it will automatically add a separating line to the next element .PARAMETER DisableCache By default, this command caches the results of its execution in the PSFramework result cache. This information can then be retrieved for the last command to do so by running Get-PSFResultCache. Setting this switch disables the caching of data in the cache. .EXAMPLE Set-PSMDParameterHelp -Path "C:\PowerShell\Projects\MyModule" -CommandName "*" -ParameterName "Foo" -HelpText @" This is some foo text For a truly foo-some result "@ Scans all files in the specified path. - Considers every function found - Will only process the parameter 'Foo' - And replace the current text with the one specified #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] Param ( [Parameter(Mandatory = $true)] [string] $Path, [Parameter(Mandatory = $true)] [string] $CommandName, [Parameter(Mandatory = $true)] [string] $ParameterName, [Parameter(Mandatory = $true)] [string] $HelpText, [switch] $DisableCache ) # Global Store for pending file updates # Exempt from Scope Boundary violation rule, since only accessed using dedicated helper function $globalFunctionHash = @{ } #region Utility Functions function Invoke-AstWalk { [CmdletBinding()] Param ( $Ast, [string] $CommandName, [string] $ParameterName, [string] $HelpText ) #Write-PSFMessage -Level Host -Message "Processing $($Ast.Extent.StartLineNumber) | $($Ast.Extent.File) | $($Ast.GetType().FullName)" $typeName = $Ast.GetType().FullName switch ($typeName) { "System.Management.Automation.Language.FunctionDefinitionAst" { if ($Ast.Name -like $CommandName) { Update-CommandParameterHelp -FunctionAst $Ast -ParameterName $ParameterName -HelpText $HelpText if ($Ast.Body.DynamicParamBlock) { Invoke-AstWalk -Ast $Ast.Body.DynamicParamBlock -CommandName $CommandName -ParameterName $ParameterName -HelpText $HelpText } if ($Ast.Body.BeginBlock) { Invoke-AstWalk -Ast $Ast.Body.BeginBlock -CommandName $CommandName -ParameterName $ParameterName -HelpText $HelpText } if ($Ast.Body.ProcessBlock) { Invoke-AstWalk -Ast $Ast.Body.ProcessBlock -CommandName $CommandName -ParameterName $ParameterName -HelpText $HelpText } if ($Ast.Body.EndBlock) { Invoke-AstWalk -Ast $Ast.Body.EndBlock -CommandName $CommandName -ParameterName $ParameterName -HelpText $HelpText } } else { Invoke-AstWalk -Ast $Ast.Body -CommandName $CommandName -ParameterName $ParameterName -HelpText $HelpText } } default { foreach ($property in $Ast.PSObject.Properties) { if ($property.Name -eq "Parent") { continue } if ($null -eq $property.Value) { continue } if (Get-Member -InputObject $property.Value -Name GetEnumerator -MemberType Method) { foreach ($item in $property.Value) { if ($item.PSObject.TypeNames -contains "System.Management.Automation.Language.Ast") { Invoke-AstWalk -Ast $item -CommandName $CommandName -ParameterName $ParameterName -HelpText $HelpText } } continue } if ($property.Value.PSObject.TypeNames -contains "System.Management.Automation.Language.Ast") { Invoke-AstWalk -Ast $property.Value -CommandName $CommandName -ParameterName $ParameterName -HelpText $HelpText } } } } } function Update-CommandParameterHelp { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "")] [CmdletBinding()] Param ( [System.Management.Automation.Language.FunctionDefinitionAst] $FunctionAst, [string] $ParameterName, [string] $HelpText ) #region Find the starting position function Get-StartIndex { [OutputType([System.Int32])] [CmdletBinding()] Param ( [System.Management.Automation.Language.FunctionDefinitionAst] $FunctionAst, [string] $ParameterName, [int] $HelpEnd ) if ($HelpEnd -lt 1) { return -1 } $index = -1 $offset = 0 while ($FunctionAst.Extent.Text.SubString(0, $HelpEnd).IndexOf(".PARAMETER $ParameterName", $offset, [System.StringComparison]::InvariantCultureIgnoreCase) -ne -1) { $tempIndex = $FunctionAst.Extent.Text.SubString(0, $HelpEnd).IndexOf(".PARAMETER $ParameterName", $offset, [System.StringComparison]::InvariantCultureIgnoreCase) $endOfLineIndex = $FunctionAst.Extent.Text.SubString(0, $HelpEnd).IndexOf("`n", $tempIndex, [System.StringComparison]::InvariantCultureIgnoreCase) if ($FunctionAst.Extent.Text.SubString($tempIndex, ($endOfLineIndex - $tempIndex)).Trim() -eq ".PARAMETER $ParameterName") { return $tempIndex } $offset = $endOfLineIndex } return $index } $startIndex = $FunctionAst.Extent.StartOffset $endIndex = $FunctionAst.Body.ParamBlock.Extent.StartOffset foreach ($attribute in $FunctionAst.Body.ParamBlock.Attributes) { if ($attribute.Extent.StartOffset -lt $endIndex) { $endIndex = $attribute.Extent.StartOffset } } $index1 = Get-StartIndex -FunctionAst $FunctionAst -ParameterName $ParameterName -HelpEnd ($endIndex - $startIndex) if ($index1 -eq -1) { Write-PSFMessage -Level Warning -Message "Could not find Comment Based Help for parameter '$ParameterName' of command '$($FunctionAst.Name)' in '$($FunctionAst.Extent.File)'" -Tag 'cbh', 'fail' -FunctionName Rename-PSMDParameter Write-Issue -Extent $FunctionAst.Extent -Type "ParameterCBHNotFound" -Data "Parameter Help not found" return } $index2 = $FunctionAst.Extent.Text.SubString(0, ($endIndex - $startIndex)).IndexOf("$ParameterName", $index1, [System.StringComparison]::InvariantCultureIgnoreCase) + $ParameterName.Length $goodIndex = $FunctionAst.Extent.Text.SubString($index2).IndexOf("`n") + 1 + $index2 #endregion Find the starting position #region Find the ending position $lines = $FunctionAst.Extent.Text.SubString(0, ($endIndex - $startIndex)).Substring($goodIndex).Split("`n") $goodLines = @() $badLine = "" foreach ($line in $lines) { if ($line -notmatch "^#{0,1}[\s`t]{0,}\.|^#>") { $goodLines += $line } else { $badLine = $line break } } if (($goodLines.Count -eq 0) -or ($goodLines.Count -eq $lines.Count)) { Write-PSFMessage -Level Warning -Message "Could not parse the Comment Based Help for parameter '$ParameterName' of command '$($FunctionAst.Name)' in '$($FunctionAst.Extent.File)'" -Tag 'cbh', 'fail' -FunctionName Rename-PSMDParameter Write-Issue -Extent $FunctionAst.Extent -Type "ParameterCBHBroken" -Data "Parameter Help cannot be parsed" return } $badIndex = $FunctionAst.Extent.Text.SubString(0, ($endIndex - $startIndex)).IndexOf($badLine, $index2) - 1 #endregion Find the ending position #region Find the indent and create the text to insert $indents = @() foreach ($line in $goodLines) { if ($line.Trim(" ^t#$([char]13)").Length -gt 0) { $line | Select-String "^(#{0,1}[\s`t]+)" | ForEach-Object { $indents += $_.Matches[0].Groups[1].Value } } } if ($indents.Count -eq 0) { $indent = "`t`t" } else { $indent = $indents | Sort-Object -Property Length | Select-Object -First 1 } $indent = $indent.Replace([char]13, [char]9) $newHelpText = ($HelpText.Split("`n") | ForEach-Object { "$($indent)$($_)" }) -join "`n" $newHelpText += "`n$($indent)" #endregion Find the indent and create the text to insert Add-FileReplacement -Path $FunctionAst.Extent.File -Start ($goodIndex + $startIndex) -Length ($badIndex - $goodIndex) -NewContent $newHelpText } function Add-FileReplacement { [CmdletBinding()] Param ( [string] $Path, [int] $Start, [int] $Length, [string] $NewContent ) Write-PSFMessage -Level Verbose -Message "Change Submitted: $Path | $Start | $Length | $NewContent" -Tag 'update', 'change', 'file' if (-not $globalFunctionHash.ContainsKey($Path)) { $globalFunctionHash[$Path] = @() } $globalFunctionHash[$Path] += New-Object PSObject -Property @{ Content = $NewContent Start = $Start Length = $Length } } function Apply-FileReplacement { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "")] [CmdletBinding()] Param ( ) foreach ($key in $globalFunctionHash.Keys) { $value = $globalFunctionHash[$key] | Sort-Object Start $content = [System.IO.File]::ReadAllText($key) $newString = "" $currentIndex = 0 foreach ($item in $value) { $newString += $content.SubString($currentIndex, ($item.Start - $currentIndex)) $newString += $item.Content $currentIndex = $item.Start + $item.Length } $newString += $content.SubString($currentIndex) [System.IO.File]::WriteAllText($key, $newString) #$newString } } function Write-Issue { [CmdletBinding()] Param ( $Extent, $Data, [string] $Type ) New-Object PSObject -Property @{ Type = $Type Data = $Data File = $Extent.File StartLine = $Extent.StartLineNumber Text = $Extent.Text } } #endregion Utility Functions $files = Get-ChildItem -Path $Path -Recurse | Where-Object Extension -Match "\.ps1|\.psm1" $issues = @() foreach ($file in $files) { $tokens = $null $parsingError = $null $ast = [System.Management.Automation.Language.Parser]::ParseFile($file.FullName, [ref]$tokens, [ref]$parsingError) Write-PSFMessage -Level VeryVerbose -Message "Updating help for <c='sub'>$CommandName / $ParameterName</c> | Scanning $($file.FullName)" -Tag 'start' -Target $Name $issues += Invoke-AstWalk -Ast $ast -CommandName $CommandName -ParameterName $ParameterName -HelpText $HelpText } Set-PSFResultCache -InputObject $issues -DisableCache $DisableCache Apply-FileReplacement $issues } function Split-PSMDScriptFile { <# .SYNOPSIS Parses a file and exports all top-level functions from it into a dedicated file, just for the function. .DESCRIPTION Parses a file and exports all top-level functions from it into a dedicated file, just for the function. The original file remains unharmed by this. Note: Any comments outside the function definition will not be copied. .PARAMETER File The file(s) to extract functions from. .PARAMETER Path The folder to export to .PARAMETER Encoding Default: UTF8 The output encoding. Can usually be left alone. .EXAMPLE PS C:\> Split-PSMDScriptFile -File ".\module.ps1" -Path .\files Exports all functions in module.ps1 and puts them in individual files in the folder .\files. #> [CmdletBinding()] Param ( [Parameter(ValueFromPipeline = $true)] [string[]] $File, [string] $Path, $Encoding = "UTF8" ) process { foreach ($item in $File) { $a = $null $b = $null $ast = [System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path $item), [ref]$a, [ref]$b) foreach ($functionAst in ($ast.EndBlock.Statements | Where-Object { $_.GetType().FullName -eq "System.Management.Automation.Language.FunctionDefinitionAst" })) { $ast.Extent.Text.Substring($functionAst.Extent.StartOffset, ($functionAst.Extent.EndOffset - $functionAst.Extent.StartOffset)) | Set-Content "$Path\$($functionAst.Name).ps1" -Encoding $Encoding } } } } function Publish-PSMDScriptFile { <# .SYNOPSIS Packages a script with all dependencies and "publishes" it as a zip package. .DESCRIPTION Packages a script with all dependencies and "publishes" it as a zip package. By default, it will be published to the user's desktop. All modules it uses will be parsed from the script: - Commands that cannot be resolved will trigger a warning. - Modules that are installed in the Windows folder (such as the ActiveDirectory module or other modules associated with server roles) will be ignored. - PSSnapins will be ignored - All other modules determined by the commands used will be provided from a repository, packaged in a subfolder and included in the zip file. If needed, the scriptfile will be modified to add the new modules folder to its list of known folders. (The source file itself will never be modified) Use Set-PSMDStagingRepository to create / use a local path for staging modules to provide that way. This gives you better control over the versions used and better performance. Also the ability to use this with non-public modules. Use Publish-PSMDStagedModule to transfer modules from path or another repository into your registered staging repository. .PARAMETER Path Path to the scriptfile to publish. The scriptfile is expected to be UTF8 encoded with BOM, otherwise some characters may end up broken. .PARAMETER OutPath The path to the folder where the output zip file will be created. Defaults to the user's desktop. .PARAMETER EnableException This parameters disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts. .EXAMPLE PS C:\> Publish-PSMDScriptFile -Path 'C:\scripts\logrotate.ps1' Creates a delivery package for the logrotate.ps1 scriptfile and places it on the desktop #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [PsfValidateScript('PSModuleDevelopment.Validate.File', ErrorString = 'PSModuleDevelopment.Validate.File')] [string] $Path, [PsfValidateScript('PSModuleDevelopment.Validate.Path', ErrorString = 'PSModuleDevelopment.Validate.Path')] [string] $OutPath = (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Script.OutPath'), [switch] $EnableException ) begin { #region Utility Functions function Get-Modifier { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Path ) $help = Get-Help $Path $modifiers = $help.alertSet.alert.Text -split "`n" | Where-Object { $_ -like "PSMD: *" } | ForEach-Object { $_ -replace '^PSMD: ' } foreach ($modifier in $modifiers) { $operation, $values = $modifier -split ":" switch ($operation) { 'Include' { foreach ($module in $values.Split(",").ForEach{ $_.Trim() }) { [pscustomobject]@{ Type = 'Include' Name = $module } } } 'Exclude' { foreach ($module in $values.Split(",").ForEach{ $_.Trim() }) { [pscustomobject]@{ Type = 'Exclude' Name = $module } } } 'IgnoreUnknownCommand' { foreach ($commandName in $values.Split(",").ForEach{ $_.Trim() }) { [pscustomobject]@{ Type = 'IgnoreCommand' Name = $commandName } } } } } } function Add-PSModulePath { [CmdletBinding()] param ( [string] $Path ) $psmodulePathCode = @' # Ensure modules are available $modulePath = "$PSScriptRoot\Modules" if (-not $env:PSModulePath.Contains($modulePath)) { $env:PSModulePath = "$($env:PSModulePath);$($modulePath)" } '@ $parsedFile = Read-PSMDScript -Path $Path $assignment = $parsedFile.Ast.FindAll({ $args[0] -is [System.Management.Automation.Language.AssignmentStatementAst] -and $args[0].Left.VariablePath.UserPath -eq 'env:PSModulePath' }, $true) if ($assignment) { return } if ($parsedFile.Ast.ParamBlock.Extent) { $paramExtent = $parsedFile.Ast.ParamBlock.Extent $text = [System.IO.File]::ReadAllText($Path) $newText = $text.Substring(0, $paramExtent.EndOffset) + $psmodulePathCode + $text.Substring($paramExtent.EndOffset) $encoding = [System.Text.UTF8Encoding]::new($true) [System.IO.File]::WriteAllText($Path, $newText, $encoding) } else { $extent = $parsedFile.Ast.EndBlock.Statements[0].Extent $text = [System.IO.File]::ReadAllText($Path) $textBefore = "" $textAfter = $text if ($extent.StartOffset -gt 0) { $textBefore = $text.Substring(0, $extent.StartOffset) $textAfter = $text.Substring($extent.StartOffset) } $newText = $textBefore + $psmodulePathCode + $textAfter $encoding = [System.Text.UTF8Encoding]::new($true) [System.IO.File]::WriteAllText($Path, $newText, $encoding) } } #endregion Utility Functions $modulesToProcess = @{ IgnoreCommand = @() Include = @() Exclude = @() } } process { #region Prepare required Modules # Scan help-notes for explicit directives $modifiers = Get-Modifier -Path $Path foreach ($modifier in $modifiers) { $modulesToProcess.$($modifier.Type) += $modifier.Name } # Detect modules needed and store them try { $parsedCommands = Get-PSMDFileCommand -Path $Path -EnableException } catch { Stop-PSFFunction -String 'Publish-PSMDScriptFile.Script.ParseError' -StringValues $Path -EnableException $EnableException -ErrorRecord $_ return } foreach ($command in $parsedCommands) { Write-PSFMessage -Level Verbose -String 'Publish-PSMDScriptFile.Script.Command' -StringValues $command.Name, $command.Count, $command.Module if ($modulesToProcess.IgnoreCommand -contains $command.Name) { continue } if (-not $command.Module -and -not $command.Internal) { Write-PSFMessage -Level Warning -String 'Publish-PSMDScriptFile.Script.Command.NotKnown' -StringValues $command.Name, $command.Count continue } if ($modulesToProcess.Exclude -contains "$($command.Module)") { continue } if ($modulesToProcess.Include -contains "$($command.Module)") { continue } if ($command.Module -is [System.Management.Automation.PSSnapInInfo]) { continue } if ($command.Module.ModuleBase -like 'C:\Windows\System32\WindowsPowerShell\v1.0*') { continue } if ($command.Module.ModuleBase -like 'C:\Program Files\PowerShell\7*') { continue } $modulesToProcess.Include += "$($command.Module)" } $tempPath = Get-PSFPath -Name Temp $newPath = New-Item -Path $tempPath -Name "PSMD_$(Get-Random)" -ItemType Directory -Force $modulesFolder = New-Item -Path $newPath.FullName -Name 'Modules' -ItemType Directory -Force foreach ($moduleLabel in $modulesToProcess.Include | Select-Object -Unique) { if (-not $moduleLabel) { continue } Invoke-PSFProtectedCommand -ActionString 'Publish-PSMDScriptFile.Module.Saving' -ActionStringValues $moduleLabel, (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Script.StagingRepository') -Scriptblock { Save-Module -Name $moduleLabel -Repository (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Script.StagingRepository') -Path $modulesFolder.FullName -ErrorAction Stop } -EnableException $EnableException -PSCmdlet $PSCmdlet -Target $moduleLabel if (Test-PSFFunctionInterrupt) { return } } #endregion Prepare required Modules # Copy script file $newScript = Copy-Item -Path $Path -Destination $newPath.FullName -PassThru # Update script to set PSModulePath Add-PSModulePath -Path $newScript.FullName # Zip result & move to destination Compress-Archive -Path "$($newPath.FullName)\*" -DestinationPath ('{0}\{1}.zip' -f $OutPath, $newScript.BaseName) -Force Remove-Item -Path $newPath.FullName -Recurse -Force -ErrorAction Ignore } } function Publish-PSMDStagedModule { <# .SYNOPSIS Publish a module to your staging repository. .DESCRIPTION Publish a module to your staging repository. Always publishes the latest version available when specifying a name. .PARAMETER Name The name of the module to publish. .PARAMETER Path The path to the module to publish. .PARAMETER Repository The repository from which to withdraw the module to then publish to the staging repository. .PARAMETER EnableException This parameters disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts. .EXAMPLE PS C:\> Publish-PSMDStagedModule -Name 'PSFramework' Publishes the latest version of PSFramework found on the local machine. .EXAMPLE PS C:\> Publish-PSMDStagedModule -Name 'Microsoft.Graph' -Repository PSGallery Publishes the entire kit of 'Microsoft.Graph' modules from the PSGallery to the staging repository. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true, ParameterSetName = 'Name')] [string] $Name, [Parameter(Mandatory = $true, ParameterSetName = 'Path')] [PsfValidateScript('PSModuleDevelopment.Validate.Path', ErrorString = 'PSModuleDevelopment.Validate.Path')] [string] $Path, [Parameter(ParameterSetName = 'Name')] [string] $Repository, [switch] $EnableException ) begin { $tempPath = Get-PSFPath -Name Temp } process { #region Explicit Path specified if ($Path) { try { Publish-Module -Path $Path -Repository (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Script.StagingRepository') -ErrorAction Stop } catch { if ($_.FullyQualifiedErrorId -like '*ModuleVersionIsAlreadyAvailableInTheGallery*') { Write-PSFMessage -Level Warning -String 'Publish-PSMDStagedModule.Module.AlreadyPublished' -StringValues $moduleToPublish.Name, $moduleToPublish.Version -ErrorRecord $_ return } Stop-PSFFunction -String 'Publish-PSMDStagedModule.Module.PublishError' -StringValues $Name, $folder.Name -ErrorRecord $_ -EnableException $EnableException return } return } #endregion Explicit Path specified #region Deploy from source repository if ($Repository) { $workingDirectory = Join-Path -Path $tempPath -ChildPath "psmd_$(Get-Random)" $null = New-Item -Path $workingDirectory -ItemType Directory -Force Save-Module -Name $Name -Repository $Repository -Path $workingDirectory foreach ($folder in Get-ChildItem -Path $workingDirectory | Sort-Object -Property LastWriteTime) { $subFolder = Get-ChildItem -Path $folder.FullName | Sort-Object -Property Name -Descending | Select-Object -First 1 try { Publish-Module -Path $subFolder.FullName -Repository (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Script.StagingRepository') -ErrorAction Stop } catch { if ($_.FullyQualifiedErrorId -like '*ModuleVersionIsAlreadyAvailableInTheGallery*') { continue } Remove-Item -Path $workingDirectory -Force -Recurse -ErrorAction Ignore Stop-PSFFunction -String 'Publish-PSMDStagedModule.Module.PublishError' -StringValues $Name, $folder.Name -ErrorRecord $_ -EnableException $EnableException return } } Remove-Item -Path $workingDirectory -Force -Recurse -ErrorAction Ignore } #endregion Deploy from source repository #region Deploy from local computer installation else { $modules = Get-Module -Name $Name -ListAvailable if (-not $modules) { Stop-PSFFunction -String 'Publish-PSMDStagedModule.Module.NotFound' -StringValues $Name -EnableException $EnableException } $moduleToPublish = $modules | Sort-Object -Property Version -Descending | Select-Object -First 1 try { Publish-Module -Path $moduleToPublish.ModuleBase -Repository (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Script.StagingRepository') -ErrorAction Stop } catch { if ($_.FullyQualifiedErrorId -like '*ModuleVersionIsAlreadyAvailableInTheGallery*') { Write-PSFMessage -Level Warning -String 'Publish-PSMDStagedModule.Module.AlreadyPublished' -StringValues $moduleToPublish.Name, $moduleToPublish.Version -ErrorRecord $_ return } Stop-PSFFunction -String 'Publish-PSMDStagedModule.Module.PublishError' -StringValues $Name, $folder.Name -ErrorRecord $_ -EnableException $EnableException return } } #endregion Deploy from local computer installation } } function Set-PSMDStagingRepository { <# .SYNOPSIS Define the repository to use for deploying modules along with scripts. .DESCRIPTION Define the repository to use for deploying modules along with scripts. By default, modules are deployed using the PSGallery, which may be problematic: - Offline computers do not have access to it - Low performance compared to a local mirror .PARAMETER Path The local path to use. Will configure that path as a PSRepository. The new repository will be named "PSMDStaging". .PARAMETER Repository The name of an existing repository to use .EXAMPLE PS C:\> Set-PSMDStagingRepository -Path 'C:\PowerShell\StagingRepo' Registers the local path 'C:\PowerShell\StagingRepo' as a repository and will use it for deploying modules along with scripts. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [Parameter(Mandatory = $true, ParameterSetName = 'Path')] [PsfValidateScript('PSModuleDevelopment.Validate.Path', ErrorString = 'PSModuleDevelopment.Validate.Path')] [string] $Path, [Parameter(Mandatory = $true, ParameterSetName = 'Repository')] [string] $Repository ) process { if ($Path) { if (Get-PSRepository -Name PSMDStaging -ErrorAction Ignore) { Unregister-PSRepository -Name PSMDStaging } Register-PSRepository -Name PSMDStaging -SourceLocation $Path -PublishLocation $Path -InstallationPolicy Trusted Set-PSFConfig -Module PSModuleDevelopment -Name 'Script.StagingRepository' -Value PSMDStaging -PassThru | Register-PSFConfig } else { Set-PSFConfig -Module PSModuleDevelopment -Name 'Script.StagingRepository' -Value $Repository -PassThru | Register-PSFConfig } } } function Get-PSMDTemplate { <# .SYNOPSIS Search for templates to create from. .DESCRIPTION Search for templates to create from. .PARAMETER TemplateName The name of the template to search for. Templates are filtered by this using wildcard comparison. Defaults to "*" (everything). .PARAMETER Store The template store to retrieve tempaltes from. By default, all stores are queried. .PARAMETER Path Instead of a registered store, look in this path for templates. .PARAMETER Tags Only return templates with the following tags. .PARAMETER Author Only return templates by this author. .PARAMETER MinimumVersion Only return templates with at least this version. .PARAMETER RequiredVersion Only return templates with exactly this version. .PARAMETER All Return all versions found. By default, only the latest matching version of a template will be returned. .PARAMETER EnableException Replaces user friendly yellow warnings with bloody red exceptions of doom! Use this if you want the function to throw terminating errors you want to catch. .EXAMPLE PS C:\> Get-PSMDTemplate Returns all templates .EXAMPLE PS C:\> Get-PSMDTemplate -TemplateName module Returns the latest version of the template named module. #> [CmdletBinding(DefaultParameterSetName = 'Store')] Param ( [Parameter(Position = 0)] [string] $TemplateName = "*", [Parameter(ParameterSetName = 'Store')] [string] $Store = "*", [Parameter(Mandatory = $true, ParameterSetName = 'Path')] [string] $Path, [string[]] $Tags, [string] $Author, [version] $MinimumVersion, [version] $RequiredVersion, [switch] $All, [switch] $EnableException ) begin { $prospects = @() } process { #region Scan folders if (Test-PSFParameterBinding -ParameterName "Path") { $templateInfos = Get-ChildItem -Path $Path -Filter "$($TemplateName)-*-Info.xml" | Where-Object { ($_.Name -replace "-\d+(\.\d+){0,3}-Info.xml$") -like $TemplateName } foreach ($info in $templateInfos) { $data = Import-PSFClixml $info.FullName $data.Path = $info.FullName -replace '-Info\.xml$','.xml' $prospects += $data } } #endregion Scan folders #region Search Stores else { $stores = Get-PsmdTemplateStore -Filter $Store foreach ($item in $stores) { if ($item.Ensure()) { $templateInfos = Get-ChildItem -Path $item.Path -Filter "$($TemplateName)-*-Info.xml" | Where-Object { ($_.Name -replace "-\d+(\.\d+){0,3}-Info.xml$") -like $TemplateName } foreach ($info in $templateInfos) { $data = Import-PSFClixml $info.FullName $data.Path = $info.FullName -replace '-Info\.xml$', '.xml' $data.Store = $item.Name $prospects += $data } } # If the user asked for a specific store, it should error out on him elseif ($item.Name -eq $Store) { Stop-PSFFunction -Message "Could not find store $Store" -EnableException $EnableException -Category OpenError -Tag 'fail','template','store','open' return } } } #endregion Search Stores } end { $filteredProspects = @() #region Apply filters foreach ($prospect in $prospects) { if ($Author) { if ($prospect.Author -notlike $Author) { continue } } if (Test-PSFParameterBinding -ParameterName MinimumVersion) { if ($prospect.Version -lt $MinimumVersion) { continue } } if (Test-PSFParameterBinding -ParameterName RequiredVersion) { if ($prospect.Version -ne $RequiredVersion) { continue } } if ($Tags) { $test = $false foreach ($tag in $Tags) { if ($prospect.Tags -contains $tag) { $test = $true break } } if (-not $test) { continue } } $filteredProspects += $prospect } #endregion Apply filters #region Return valid templates if ($All) { return $filteredProspects | Sort-Object Type, Name, Version } $prospectHash = @{ } foreach ($prospect in $filteredProspects) { if ($prospectHash.Keys -notcontains $prospect.Name) { $prospectHash[$prospect.Name] = $prospect } elseif ($prospectHash[$prospect.Name].Version -lt $prospect.Version) { $prospectHash[$prospect.Name] = $prospect } } $prospectHash.Values | Sort-Object Type, Name #endregion Return valid templates } } function Invoke-PSMDTemplate { <# .SYNOPSIS Creates a project/file from a template. .DESCRIPTION This function takes a template and turns it into a finished file&folder structure. It does so by creating the files and folders stored within, replacing all parameters specified with values provided by the user. Missing parameters will be prompted for. .PARAMETER Template The template object to build from. Accepts objects returned by Get-PSMDTemplate. .PARAMETER TemplateName The name of the template to build from. Warning: This does wildcard interpretation, don't specify '*' unless you like answering parameter prompts. .PARAMETER Store The template store to retrieve tempaltes from. By default, all stores are queried. .PARAMETER Path Instead of a registered store, look in this path for templates. .PARAMETER OutPath The path in which to create the output. By default, it will create in the current directory. .PARAMETER Name The name of the produced output. Automatically inserted for any name parameter specified on creation. Also used for creating a root folder, when creating a project. .PARAMETER NoFolder Skip automatic folder creation for project templates. By default, this command will create a folder to place files&folders in when creating a project. .PARAMETER Encoding The encoding to apply to text files. The default setting for this can be configured by updating the 'PSFramework.Text.Encoding.DefaultWrite' configuration setting. The initial default value is utf8 with BOM. .PARAMETER Parameters A Hashtable containing parameters for use in creating the template. .PARAMETER Raw By default, all parameters will be replaced during invocation. In Raw mode, this is skipped, reproducing mostly the original template input (dynamic scriptblocks will now be named scriptblocks)). .PARAMETER GenerateObjects By default, Invoke-PSMDTemplate generates files. In GenerateObjects mode, no file but objects are created. .PARAMETER Force If the target path the template should be written to (filename or folder name within $OutPath), then overwrite it. By default, this function will fail if an overwrite is required. .PARAMETER Silent This places the function in unattended mode, causing it to error on anything requiring direct user input. .PARAMETER NoConfigFile By default, this command will look in the execution path and above for files named "PSMDConfig.psd1" to populate template parameters from. .PARAMETER EnableException Replaces user friendly yellow warnings with bloody red exceptions of doom! Use this if you want the function to throw terminating errors you want to catch. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .EXAMPLE PS C:\> Invoke-PSMDTemplate -TemplateName "module" Creates a project based on the module template in the current folder, asking for all details. .EXAMPLE PS C:\> Invoke-PSMDTemplate -TemplateName "module" -Name "MyModule" Creates a project based on the module template with the name "MyModule" .EXAMPLE PS C:\> Invoke-PSMDTemplate MiniModule -Parameters @{ Author = 'Fred' } Creates a new project based on the template MiniModule and predefines the value for the "Author" placeholder. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSPossibleIncorrectUsageOfAssignmentOperator", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')] [OutputType([PSModuleDevelopment.Template.TemplateResult])] [Alias('imt')] [CmdletBinding(SupportsShouldProcess = $true)] param ( [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'NameStore')] [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'NamePath')] [string] $TemplateName, [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'Template')] [PSModuleDevelopment.Template.TemplateInfo[]] $Template, [Parameter(ParameterSetName = 'NameStore')] [string] $Store = "*", [Parameter(Mandatory = $true, ParameterSetName = 'NamePath')] [string] $Path, [Parameter(Position = 2)] [PSFramework.Validation.PsfValidateScript('PSFramework.Validate.FSPath.Folder', ErrorString = 'PSFramework.Validate.FSPath.Folder')] [string] $OutPath = (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Template.OutPath' -Fallback "."), [Parameter(Position = 1)] [string] $Name, [PSFEncoding] $Encoding = (Get-PSFConfigValue -FullName 'PSFramework.Text.Encoding.DefaultWrite'), [switch] $NoFolder, [hashtable] $Parameters = @{ }, [switch] $Raw, [switch] $GenerateObjects, [switch] $Force, [switch] $Silent, [switch] $NoConfigFile, [switch] $EnableException ) begin { $resolvedPath = Resolve-PSFPath -Path $OutPath $templates = @() switch ($PSCmdlet.ParameterSetName) { 'NameStore' { $templates = Get-PSMDTemplate -TemplateName $TemplateName -Store $Store } 'NamePath' { $templates = Get-PSMDTemplate -TemplateName $TemplateName -Path $Path } } if ($TemplateName -and -not $templates) { Stop-PSFFunction -String 'Invoke-PSMDTemplate.Template.NotFound' -StringValues $TemplateName -EnableException $EnableException -Cmdlet $PSCmdlet return } #region Parameter Processing if (-not $Parameters) { $Parameters = @{ } } if ($Name) { $Parameters["Name"] = $Name } if ($NoConfigFile) { $paramCloned = Resolve-TemplateParameter -Configuration $Parameters -FromConfiguration} else { $paramCloned = Resolve-TemplateParameter -Path $resolvedPath -Configuration $Parameters -FromConfiguration } #endregion Parameter Processing #region Helper function function Invoke-Template { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [PSModuleDevelopment.Template.TemplateInfo] $Template, [string] $OutPath, [PSFEncoding] $Encoding, [bool] $NoFolder, [hashtable] $Parameters, [bool] $Raw, [switch] $GenerateObjects, [bool] $Silent ) Write-PSFMessage -Level Verbose -Message "Processing template $($item)" -Tag 'template', 'invoke' -FunctionName Invoke-PSMDTemplate $templateData = Import-PSFClixml -Path $Template.Path -ErrorAction Stop #region Process Parameters foreach ($parameter in $templateData.Parameters) { if (-not $parameter) { continue } if (-not $Parameters.ContainsKey($parameter)) { if ($Silent) { throw "Parameter not specified: $parameter" } try { $value = Read-Host -Prompt "Enter value for parameter '$parameter'" -ErrorAction Stop $Parameters[$parameter] = $value } catch { throw } } } #endregion Process Parameters #region Scripts $scriptParameters = @{ } if (-not $Raw) { foreach ($scriptParam in $templateData.Scripts.Values) { if (-not $scriptParam) { continue } try { $scriptParameters[$scriptParam.Name] = "$([scriptblock]::Create($scriptParam.StringScript).Invoke())" } catch { if ($Silent) { throw (New-Object System.Exception("Scriptblock $($scriptParam.Name) failed during execution: $_", $_.Exception)) } else { Write-PSFMessage -Level Warning -Message "Scriptblock $($scriptParam.Name) failed during execution. Please specify a custom value or use CTRL+C to terminate creation" -ErrorRecord $_ -FunctionName "Invoke-PSMDTemplate" -ModuleName 'PSModuleDevelopment' $scriptParameters[$scriptParam.Name] = Read-Host -Prompt "Value for script $($scriptParam.Name)" } } } } #endregion Scripts $createdTemplateItems = switch ($templateData.Type.ToString()) { #region File "File" { foreach ($child in $templateData.Children) { New-TemplateItem -Item $child -Path $OutPath -ParameterFlat $Parameters -ParameterScript $scriptParameters -Raw $Raw } if ($Raw -and $templateData.Scripts.Values) { $templateData.Scripts.Values | Export-Clixml -Path (Join-Path $OutPath "_PSMD_ParameterScripts.xml") } } #endregion File #region Project "Project" { #region Resolve output folder if (-not $NoFolder) { if ($Parameters["Name"]) { $projectName = $Parameters["Name"] $projectFullName = Join-Path $OutPath $projectName if ((Test-Path $projectFullName) -and (-not $Force)) { throw "Project root folder already exists: $projectFullName" } $newFolder = New-Item -Path $OutPath -Name $Parameters["Name"] -ItemType Directory -ErrorAction Stop -Force } else { throw "Parameter Name is needed to create a project without setting the -NoFolder parameter!" } } else { $newFolder = Get-Item $OutPath } #endregion Resolve output folder foreach ($child in $templateData.Children) { New-TemplateItem -Item $child -Path $newFolder.FullName -ParameterFlat $Parameters -ParameterScript $scriptParameters -Raw $Raw } #region Write Config File (Raw) if ($Raw) { $guid = [System.Guid]::NewGuid().ToString() $optionsTemplate = @" @{ TemplateName = "$($Template.Name)" Version = ([Version]"$($Template.Version)") Tags = $(($Template.Tags | ForEach-Object { "'$_'" }) -join ",") Author = "$($Template.Author)" Description = "$($Template.Description)" þþþPLACEHOLDER-$($guid)þþþ } "@ if ($params = $templateData.Scripts.Values) { $list = @() foreach ($param in $params) { $list += @" $($param.Name) = { $($param.StringScript) } "@ } $optionsTemplate = $optionsTemplate -replace "þþþPLACEHOLDER-$($guid)þþþ", ($list -join "`n`n") } else { $optionsTemplate = $optionsTemplate -replace "þþþPLACEHOLDER-$($guid)þþþ", "" } [PSModuleDevelopment.Template.TemplateResult]@{ Name = "PSMDTemplate.ps1" Path = $newFolder.FullName FullPath = (Join-Path $newFolder.FullName "PSMDTemplate.ps1") Content = $optionsTemplate } } #endregion Write Config File (Raw) } #endregion Project } If ($GenerateObjects) { return $createdTemplateItems } Write-TemplateResult -TemplateResult $createdTemplateItems -Encoding $Encoding } function New-TemplateItem { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [OutputType([PSModuleDevelopment.Template.TemplateResult])] [CmdletBinding()] param ( # Fixing that in the next PSFramework release: https://github.com/PowershellFrameworkCollective/psframework/issues/646 #[PSFramework.Utility.ScriptTransformation('PSModuleDevelopment.TemplateItem', [PSModuleDevelopment.Template.TemplateItemBase])] #[PSModuleDevelopment.Template.TemplateItemBase] $Item, [string] $Path, [hashtable] $ParameterFlat, [hashtable] $ParameterScript, [bool] $Raw ) Write-PSFMessage -Level Verbose -Message "Creating Template-Item: $($Item.Name) ($($Item.RelativePath))" -FunctionName Invoke-PSMDTemplate -ModuleName PSModuleDevelopment -Tag 'create', 'template' $identifier = $Item.Identifier $isFile = $Item.PSObject.Properties.Name -contains 'Value' #region File if ($isFile) { $fileName = $Item.Name if (-not $Raw) { foreach ($param in $Item.FileSystemParameterFlat) { $fileName = [PSModuleDevelopment.Utility.UtilityHost]::Replace($fileName, "$($identifier)$($param)$($identifier)", $ParameterFlat[$param], $false) } foreach ($param in $Item.FileSystemParameterScript) { $fileName = [PSModuleDevelopment.Utility.UtilityHost]::Replace($fileName, "$($identifier)$($param)$($identifier)", $ParameterScript[$param], $false) } } $destPath = Join-Path $Path $fileName if ($Item.PlainText) { $text = $Item.Value if (-not $Raw) { foreach ($param in $Item.ContentParameterFlat) { $text = [PSModuleDevelopment.Utility.UtilityHost]::Replace($text, "$($identifier)$($param)$($identifier)", $ParameterFlat[$param], $false) } foreach ($param in $Item.ContentParameterScript) { $text = [PSModuleDevelopment.Utility.UtilityHost]::Replace($text, "$($identifier)!$($param)!$($identifier)", $ParameterScript[$param], $false) } } return [PSModuleDevelopment.Template.TemplateResult]@{ Name = $fileName Path = $Path FullPath = $destPath Content = $text } } else { $bytes = [System.Convert]::FromBase64String($Item.Value) return [PSModuleDevelopment.Template.TemplateResult]@{ Name = $fileName Path = $Path FullPath = $destPath Content = $bytes IsText = $false } } } #endregion File #region Folder else { $folderName = $Item.Name if (-not $Raw) { foreach ($param in $Item.FileSystemParameterFlat) { $folderName = $folderName -replace "$($identifier)$([regex]::Escape($param))$($identifier)", $ParameterFlat[$param] } foreach ($param in $Item.FileSystemParameterScript) { $folderName = $folderName -replace "$($identifier)!$([regex]::Escape($param))!$($identifier)", $ParameterScript[$param] } } $folder = Join-Path -Path $Path -ChildPath $folderName # Return a folder object to make sure empty folders are not excluded [PSModuleDevelopment.Template.TemplateResult]@{ Name = $folderName Path = $Path FullPath = $folder IsFolder = $true IsText = $false } foreach ($child in $Item.Children) { New-TemplateItem -Item $child -Path $folder -ParameterFlat $ParameterFlat -ParameterScript $ParameterScript -Raw $Raw } } #endregion Folder } function Write-TemplateResult { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [PSModuleDevelopment.Template.TemplateResult[]] $TemplateResult, [PSFEncoding] $Encoding ) $msgParam = @{ Level = 'Verbose'; FunctionName = 'Invoke-PSMDTemplate' } foreach ($item in $TemplateResult | Sort-Object { $_.FullPath.Length }) { Write-PSFMessage @msgParam -Message "Creating file: $($item.FullPath)" -Tag 'create', 'template' if (-not (Test-Path $item.Path)) { Write-PSFMessage -Level Verbose -Message "Creating Folder $($item.Path)" $null = New-Item -Path $item.Path -ItemType Directory } if ($item.IsFolder) { if (-not (Test-Path $item.FullPath)) { Write-PSFMessage @msgParam -Message "Creating Folder $($item.FullPath)" $null = New-Item -Path $item.FullPath -ItemType Directory } continue } if ($item.IsText) { Write-PSFMessage @msgParam -Message "Creating as a Text-File" [System.IO.File]::WriteAllText($item.FullPath, $item.Content, $Encoding) } else { Write-PSFMessage @msgParam -Message "Creating as a Binary-File" [System.IO.File]::WriteAllBytes($item.FullPath, $item.Content) } } } #endregion Helper function } process { if (Test-PSFFunctionInterrupt) { return } $invokeParam = @{ Parameters = $paramCloned OutPath = $resolvedPath NoFolder = $NoFolder Encoding = $Encoding Raw = $Raw Silent = $Silent GenerateObjects = $GenerateObjects } foreach ($item in $Template) { Invoke-PSFProtectedCommand -ActionString 'Invoke-PSMDTemplate.Invoking' -ActionStringValues $item -Target $item -ScriptBlock { Invoke-Template @invokeParam -Template $item } -EnableException $EnableException -PSCmdlet $PSCmdlet -Continue } foreach ($item in $templates) { Invoke-PSFProtectedCommand -ActionString 'Invoke-PSMDTemplate.Invoking' -ActionStringValues $item -Target $item -ScriptBlock { Invoke-Template @invokeParam -Template $item } -EnableException $EnableException -PSCmdlet $PSCmdlet -Continue } } } function New-PSMDDotNetProject { <# .SYNOPSIS Wrapper function around 'dotnet new' .DESCRIPTION This function is a wrapper around the dotnet.exe application with the parameter 'new'. It can be used to create projects from templates, as well as to administrate templates. .PARAMETER TemplateName The name of the template to create .PARAMETER List List the existing templates. .PARAMETER Help Ask for help / documentation. Particularly useful when dealing with project types that have a lot of options. .PARAMETER Force Overwrite existing files. .PARAMETER Name The name of the project to create .PARAMETER Output The folder in which to create it. Note: This folder will automatically be te root folder of the project. If this folder doesn't exist yet, it will be created. When used with -Force, it will automatically purge all contents. .PARAMETER Install Install the specified template from the VS marketplace. .PARAMETER Uninstall Uninstall an installed template. .PARAMETER Arguments Additional arguments to pass to the application. Generally used for parameters when creating a project from a template. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .EXAMPLE PS C:\> dotnetnew -l Lists all installed templates. .EXAMPLE PS C:\> dotnetnew mvc foo F:\temp\projects\foo -au Windows --no-restore Creates a new MVC project named "foo" in folder "F:\Temp\projects\foo" - It will set authentication to windows - It will skip the automatic restore of the project on create #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')] [Alias('dotnetnew')] [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = 'Create')] Param ( [Parameter(Position = 0, Mandatory = $true, ParameterSetName = 'Create')] [Parameter(Position = 0, ParameterSetName = 'List')] [string] $TemplateName, [Parameter(ParameterSetName = 'List')] [Alias('l')] [switch] $List, [Alias('h')] [switch] $Help, [switch] $Force, [Parameter(Position = 1, ParameterSetName = 'Create')] [Alias('n')] [string] $Name, [Parameter(Position = 2, ParameterSetName = 'Create')] [Alias('o')] [string] $Output, [Parameter(Mandatory = $true, ParameterSetName = 'Install')] [Alias('i')] [string] $Install, [Parameter(Mandatory = $true, ParameterSetName = 'Uninstall')] [Alias('u')] [string] $Uninstall, [Parameter(ValueFromRemainingArguments = $true)] [Alias('a')] [string[]] $Arguments ) begin { $parset = $PSCmdlet.ParameterSetName Write-PSFMessage -Level InternalComment -Message "Active parameterset: $parset" -Tag 'start' if (-not (Get-Command dotnet.exe)) { throw "Could not find dotnet.exe! This should automatically be available on machines with Visual Studio installed." } $dotNetArgs = @() switch ($parset) { 'Create' { if (Test-PSFParameterBinding -ParameterName TemplateName) { $dotNetArgs += $TemplateName } if ($Help) { $dotNetArgs += "-h" } if (Test-PSFParameterBinding -ParameterName Name) { $dotNetArgs += "-n" $dotNetArgs += $Name } if (Test-PSFParameterBinding -ParameterName Output) { $dotNetArgs += "-o" $dotNetArgs += $Output } if ($Force) { $dotNetArgs += "--Force" } } 'List' { if (Test-PSFParameterBinding -ParameterName TemplateName) { $dotNetArgs += $TemplateName } $dotNetArgs += '-l' if ($Help) { $dotNetArgs += "-h" } } 'Install' { $dotNetArgs += '-i' $dotNetArgs += $Install if ($Help) { $dotNetArgs += '-h'} } 'Uninstall' { $dotNetArgs += '-u' $dotNetArgs += $Uninstall if ($Help) { $dotNetArgs += '-h' } } } foreach ($item in $Arguments) { $dotNetArgs += $item } Write-PSFMessage -Level Verbose -Message "Resolved arguments: $($dotNetArgs -join " ")" -Tag 'argument','start' } process { if ($PSCmdlet.ShouldProcess("dotnet", "Perform action: $parset")) { if ($parset -eq 'Create') { if ($Output) { if ((Test-Path $Output) -and $Force) { $null = New-Item $Output -ItemType Directory -Force -ErrorAction Stop } if (-not (Test-Path $Output)) { $null = New-Item $Output -ItemType Directory -Force -ErrorAction Stop } } } Write-PSFMessage -Level Verbose -Message "Executing with arguments: $($dotNetArgs -join " ")" -Tag 'argument', 'start' & dotnet.exe new $dotNetArgs } } } function New-PSMDTemplate { <# .SYNOPSIS Creates a template from a reference file / folder. .DESCRIPTION This function creates a template based on an existing folder or file. It automatically detects parameters that should be filled in one creation time. # Template reference: # #---------------------# Project templates can be preconfigured by a special reference file in the folder root. This file must be named "PSMDTemplate.ps1" and will not be part of the template. It must emit a single hashtable with various pieces of information. This hashtable can have any number of the following values, in any desired combination: - Scripts: A Hashtable, of scriptblocks. These are scripts used for replacement parameters, the key is the name used on insertions. - TemplateName: Name of the template - Version: The version number for the template (See AutoIncrementVersion property) - AutoIncrementVersion: Whether the version number should be incremented - Tags: Tags to add to a template - makes searching and finding templates easier - Author: Name of the author of the template - Description: Description of the template - Exclusions: List of relative file/folder names to not process / skip. Each of those entries can also be overridden by specifying the corresponding parameter of this function. # Parameterizing templates: # #---------------------------# The script will pick up any parameter found in the files and folders (including the file/folder name itself). There are three ways to do this: - Named text replacement: The user will need to specify what to insert into this when creating a new project from this template. - Scriptblock replacement: The included scriptblock will be executed on initialization, in order to provide a text to insert. Duplicate scriptblocks will be merged. - Named scriptblock replacement: The template reference file can define scriptblocks, their value will be inserted here. The same name can be reused any number of times across the entire project, it will always receive the same input. Naming Rules: - Parameter names cannot include the characters '!', '{', or '}' - Parameter names cannot include the parameter identifier. This is by default 'þ'. This identifier can be changed by updating the 'psmoduledevelopment.template.identifier' configuration setting. - Names are not case sensitive. Examples: ° Named for replacement: "Test þnameþ" --> "Test <inserted text of parameter>" ° Scriptblock replacement: "Test þ{ $env:COMPUTERNAME }þ" --> "Test <Name of invoking computer>" - Important: No space between identifier and curly braces! - Scriptblock can have multiple lines. ° Named Scriptblock replacement: "Test þ!ClosestDomainController!þ" --> "Test <Result of script ClosestDomainController>" - Named Scriptblocks are created by using a template reference file (see section above) .PARAMETER ReferencePath Root path in which all files are selected for creating a template project. The folder will not be part of the template, only its content. .PARAMETER FilePath Path to a single file. Used to create a template for that single file, instead of a full-blown project. Note: Does not support template reference files. .PARAMETER TemplateName Name of the template. .PARAMETER Filter Only files matching this filter will be included in the template. .PARAMETER OutStore Where the template will be stored at. By default, it will push the template to the default store (A folder in appdata unless configuration was changed). .PARAMETER OutPath If the template should be written to a specific path instead. Specify a folder. .PARAMETER Exclusions The relative path of the files or folders to ignore. Ignoring folders will also ignore all items in the folder. .PARAMETER Version The version of the template. .PARAMETER Author The author of the template. .PARAMETER Description A description text for the template itself. This will be visible to the user before invoking the template and should describe what this template is for. .PARAMETER Tags Tags to apply to the template, making it easier to filter & search. .PARAMETER Force If the template in the specified version in the specified destination already exists, this will fail unless the Force parameter is used. .PARAMETER EnableException Replaces user friendly yellow warnings with bloody red exceptions of doom! Use this if you want the function to throw terminating errors you want to catch. .EXAMPLE PS C:\> New-PSMDTemplate -FilePath .\þnameþ.Test.ps1 -TemplateName functiontest Creates a new template named 'functiontest', based on the content of '.\þnameþ.Test.ps1' #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding(DefaultParameterSetName = 'Project')] param ( [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'Project')] [string] $ReferencePath, [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'File')] [string] $FilePath, [Parameter(Position = 1, ParameterSetName = 'Project')] [Parameter(Position = 1, ParameterSetName = 'File', Mandatory = $true)] [string] $TemplateName, [string] $Filter = "*", [string] $OutStore = "Default", [string] $OutPath, [string[]] $Exclusions, [version] $Version = "1.0.0.0", [string] $Description, [string] $Author = (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Template.ParameterDefault.Author' -Fallback $env:USERNAME), [string[]] $Tags, [switch] $Force, [switch] $EnableException ) begin { #region Insert basic meta-data $identifier = [regex]::Escape(( Get-PSFConfigValue -FullName 'psmoduledevelopment.template.identifier' -Fallback 'þ' )) $binaryExtensions = Get-PSFConfigValue -FullName 'PSModuleDevelopment.Template.BinaryExtensions' -Fallback @('.dll', '.exe', '.pdf', '.doc', '.docx', '.xls', '.xlsx') $template = New-Object PSModuleDevelopment.Template.Template $template.Name = $TemplateName $template.Version = $Version $template.Tags = $Tags $template.Description = $Description $template.Author = $Author if ($PSCmdlet.ParameterSetName -eq 'File') { $template.Type = 'File' } else { $template.Type = 'Project' $processedReferencePath = Resolve-Path $ReferencePath if (Test-Path (Join-Path $processedReferencePath "PSMDTemplate.ps1")) { $templateData = & (Join-Path $processedReferencePath "PSMDTemplate.ps1") foreach ($key in $templateData.Scripts.Keys) { $template.Scripts[$key] = New-Object PSModuleDevelopment.Template.ParameterScript($key, $templateData.Scripts[$key]) } if ($templateData.TemplateName -and (Test-PSFParameterBinding -ParameterName TemplateName -Not)) { $template.Name = $templateData.TemplateName } if ($templateData.Version -and (Test-PSFParameterBinding -ParameterName Version -Not)) { $template.Version = $templateData.Version } if ($templateData.Tags -and (Test-PSFParameterBinding -ParameterName Tags -Not)) { $template.Tags = $templateData.Tags } if ($templateData.Description -and (Test-PSFParameterBinding -ParameterName Description -Not)) { $template.Description = $templateData.Description } if ($templateData.Author -and (Test-PSFParameterBinding -ParameterName Author -Not)) { $template.Author = $templateData.Author } if (-not $template.Name) { Stop-PSFFunction -Message "No template name detected: Make sure to specify it as parameter or include it in the 'PSMDTemplate.ps1' definition file!" -EnableException $EnableException return } if ($templateData.AutoIncrementVersion) { $oldTemplate = Get-PSMDTemplate -TemplateName $template.Name -WarningAction SilentlyContinue | Sort-Object Version | Select-Object -First 1 if (($oldTemplate) -and ($oldTemplate.Version -ge $template.Version)) { $major = $oldTemplate.Version.Major $minor = $oldTemplate.Version.Minor $revision = $oldTemplate.Version.Revision $build = $oldTemplate.Version.Build # Increment lowest element if ($build -ge 0) { $build++ } elseif ($revision -ge 0) { $revision++ } elseif ($minor -ge 0) { $minor++ } else { $major++ } $template.Version = "$($major).$($minor).$($revision).$($build)" -replace "\.-1",'' } } if ($templateData.Exclusions -and (Test-PSFParameterBinding -ParameterName Exclusions -Not)) { $Exclusions = $templateData.Exclusions } } if ($Exclusions) { $oldExclusions = $Exclusions $Exclusions = @() foreach ($exclusion in $oldExclusions) { $Exclusions += Join-Path $processedReferencePath $exclusion } } } #endregion Insert basic meta-data #region Validation #region Validate FilePath if ($FilePath) { if (-not (Test-Path $FilePath -PathType Leaf)) { Stop-PSFFunction -Message "Filepath $FilePath is invalid. Ensure it exists and is a file" -EnableException $EnableException -Category InvalidArgument -Tag 'fail', 'argument', 'path' return } } #endregion Validate FilePath #region Validate & ensure output folder $fileName = "$($template.Name)-$($template.Version).xml" $infoFileName = "$($template.Name)-$($template.Version)-Info.xml" if ($OutPath) { $exportFolder = $OutPath } else { $exportFolder = Get-PsmdTemplateStore -Filter $OutStore | Select-Object -ExpandProperty Path -First 1 } if (-not $exportFolder) { Stop-PSFFunction -Message "Unable to resolve a path to create the template in. Verify a valid template store or path were specified." -Category InvalidArgument -EnableException $EnableException -Tag 'fail', 'argument', 'path' return } if (-not (Test-Path $exportFolder)) { if ($Force) { try { $null = New-Item -Path $exportFolder -ItemType Directory -Force -ErrorAction Stop } catch { Stop-PSFFunction -Message "Failed to create output path: $exportFolder" -ErrorRecord $_ -Tag 'fail', 'folder', 'create' -EnableException $EnableException return } } else { Stop-PSFFunction -Message "Output folder does not exist. Use '-Force' to have this function automatically create it: $exportFolder" -Category InvalidArgument -EnableException $EnableException -Tag 'fail', 'argument', 'path' return } } if ((Test-Path (Join-Path $exportFolder $fileName)) -and (-not $Force)) { Stop-PSFFunction -Message "Template already exists in the current version. Use '-Force' if you want to overwrite it!" -Category InvalidArgument -EnableException $EnableException -Tag 'fail', 'argument', 'path' return } #endregion Validate & ensure output folder #endregion Validation #region Utility functions function Convert-Item { [CmdletBinding()] param ( [System.IO.FileSystemInfo] $Item, [PSModuleDevelopment.Template.TemplateItemBase] $Parent, [string] $Filter, [string[]] $Exclusions, [PSModuleDevelopment.Template.Template] $Template, [string] $ReferencePath, [string] $Identifier, [string[]] $BinaryExtensions ) if ($Item.FullName -in $Exclusions) { return } #region Regex <# Fixed string Replacement pattern: "$($Identifier)([^{}!]+?)$($Identifier)" Named script replacement pattern: "$($Identifier)!([^{}!]+?)!$($Identifier)" Live script replacement pattern: "$($Identifier){(.+?)}$($Identifier)" Chained together in a logical or, in order to avoid combination issues. #> $pattern = "$($Identifier)([^{}!]+?)$($Identifier)|$($Identifier)!([^{}!]+?)!$($Identifier)|(?ms)$($Identifier){(.+?)}$($Identifier)" #endregion Regex $name = $Item.Name $relativePath = "" if ($ReferencePath) { $relativePath = ($Item.FullName -replace "^$([regex]::Escape($ReferencePath))","").Trim("\") } #region Folder if ($Item.GetType().Name -eq "DirectoryInfo") { $object = New-Object PSModuleDevelopment.Template.TemplateItemFolder $object.Name = $name $object.RelativePath = $relativePath foreach ($find in ([regex]::Matches($name, $pattern, 'IgnoreCase'))) { #region Fixed string replacement if ($find.Groups[1].Success) { if ($object.FileSystemParameterFlat -notcontains $find.Groups[1].Value) { $null = $object.FileSystemParameterFlat.Add($find.Groups[1].Value) } if ($Template.Parameters -notcontains $find.Groups[1].Value) { $null = $Template.Parameters.Add($find.Groups[1].Value) } } #endregion Fixed string replacement #region Named Scriptblock replacement if ($find.Groups[2].Success) { $scriptName = $find.Groups[2].Value if ($Template.Scripts.Keys -eq $scriptName) { $object.FileSystemParameterScript($scriptName) } else { throw "Unknown named scriptblock '$($scriptName)' in name of '$($Item.FullName)'. Make sure the named scriptblock exists in the configuration file." } } #endregion Named Scriptblock replacement } foreach ($child in (Get-ChildItem -Path $Item.FullName -Filter $Filter)) { $paramConvertItem = @{ Item = $child Filter = $Filter Exclusions = $Exclusions Template = $Template ReferencePath = $ReferencePath Identifier = $Identifier BinaryExtensions = $BinaryExtensions Parent = $object } Convert-Item @paramConvertItem } } #endregion Folder #region File else { $object = New-Object PSModuleDevelopment.Template.TemplateItemFile $object.Name = $name $object.RelativePath = $relativePath #region File Name foreach ($find in ([regex]::Matches($name, $pattern, 'IgnoreCase'))) { #region Fixed string replacement if ($find.Groups[1].Success) { if ($object.FileSystemParameterFlat -notcontains $find.Groups[1].Value) { $null = $object.FileSystemParameterFlat.Add($find.Groups[1].Value) } if ($Template.Parameters -notcontains $find.Groups[1].Value) { $null = $Template.Parameters.Add($find.Groups[1].Value) } } #endregion Fixed string replacement #region Named Scriptblock replacement if ($find.Groups[2].Success) { $scriptName = $find.Groups[2].Value if ($Template.Scripts.Keys -eq $scriptName) { $null = $object.FileSystemParameterScript.Add($scriptName) } else { throw "Unknown named scriptblock '$($scriptName)' in name of '$($Item.FullName)'. Make sure the named scriptblock exists in the configuration file." } } #endregion Named Scriptblock replacement } #endregion File Name #region File Content if (-not ($Item.Extension -in $BinaryExtensions)) { $text = [System.IO.File]::ReadAllText($Item.FullName) foreach ($find in ([regex]::Matches($text, $pattern, 'IgnoreCase, Multiline'))) { #region Fixed string replacement if ($find.Groups[1].Success) { if ($object.ContentParameterFlat -notcontains $find.Groups[1].Value) { $null = $object.ContentParameterFlat.Add($find.Groups[1].Value) } if ($Template.Parameters -notcontains $find.Groups[1].Value) { $null = $Template.Parameters.Add($find.Groups[1].Value) } } #endregion Fixed string replacement #region Named Scriptblock replacement if ($find.Groups[2].Success) { $scriptName = $find.Groups[2].Value if ($Template.Scripts.Keys -eq $scriptName) { $null = $object.ContentParameterScript.Add($scriptName) } else { throw "Unknown named scriptblock '$($scriptName)' in name of '$($Item.FullName)'. Make sure the named scriptblock exists in the configuration file." } } #endregion Named Scriptblock replacement #region Live Scriptblock replacement if ($find.Groups[3].Success) { $scriptCode = $find.Groups[3].Value $scriptBlock = [ScriptBlock]::Create($scriptCode) if ($scriptBlock.ToString() -in $Template.Scripts.Values.StringScript) { $scriptName = ($Template.Scripts.Values | Where-Object StringScript -EQ $scriptBlock.ToString() | Select-Object -First 1).Name if ($object.ContentParameterScript -notcontains $scriptName) { $null = $object.ContentParameterScript.Add($scriptName) } $text = $text -replace ([regex]::Escape("$($Identifier){$($scriptCode)}$($Identifier)")), "$($Identifier)!$($scriptName)!$($Identifier)" } else { do { $scriptName = "dynamicscript_$(Get-Random -Minimum 100000 -Maximum 999999)" } until ($Template.Scripts.Keys -notcontains $scriptName) $parameter = New-Object PSModuleDevelopment.Template.ParameterScript($scriptName, ([System.Management.Automation.ScriptBlock]::Create($scriptCode))) $Template.Scripts[$scriptName] = $parameter $null = $object.ContentParameterScript.Add($scriptName) $text = $text -replace ([regex]::Escape("$($Identifier){$($scriptCode)}$($Identifier)")), "$($Identifier)!$($scriptName)!$($Identifier)" } } #endregion Live Scriptblock replacement } $object.Value = $text } else { $bytes = [System.IO.File]::ReadAllBytes($Item.FullName) $object.Value = [System.Convert]::ToBase64String($bytes) $object.PlainText = $false } #endregion File Content } #endregion File # Set identifier, so that Invoke-PSMDTemplate knows what to use when creating the item # Needed for sharing templates between users with different identifiers $object.Identifier = $Identifier if ($Parent) { $null = $Parent.Children.Add($object) } else { $null = $Template.Children.Add($object) } } #endregion Utility functions } process { if (Test-PSFFunctionInterrupt) { return } #region Parse content and produce template if ($ReferencePath) { foreach ($item in (Get-ChildItem -Path $processedReferencePath -Filter $Filter)) { if ($item.FullName -in $Exclusions) { continue } if ($item.Name -eq "PSMDTemplate.ps1") { continue } Convert-Item -Item $item -Filter $Filter -Exclusions $Exclusions -Template $template -ReferencePath $processedReferencePath -Identifier $identifier -BinaryExtensions $binaryExtensions } } else { $item = Get-Item -Path $FilePath Convert-Item -Item $item -Template $template -Identifier $identifier -BinaryExtensions $binaryExtensions } #endregion Parse content and produce template } end { if (Test-PSFFunctionInterrupt) { return } $template.CreatedOn = (Get-Date).Date $template | Export-PSFClixml -Path (Join-Path $exportFolder $fileName) -Depth 99 $template.ToTemplateInfo() | Export-PSFClixml -Path (Join-Path $exportFolder $infoFileName) } } function Remove-PSMDTemplate { <# .SYNOPSIS Removes templates .DESCRIPTION This function removes templates used in the PSModuleDevelopment templating system. .PARAMETER Template A template object returned by Get-PSMDTemplate. Will clear exactly the version specified, from exactly its location. .PARAMETER TemplateName The name of the template to remove. Templates are filtered by this using wildcard comparison. .PARAMETER Store The template store to retrieve tempaltes from. By default, all stores are queried. .PARAMETER Path Instead of a registered store, look in this path for templates. .PARAMETER Deprecated Will delete all versions of matching templates except for the latest one. Note: If the same template is found in multiple stores, it will keep a single copy across all stores. To process by store, be sure to specify the store parameter and loop over the stores desired. .PARAMETER EnableException Replaces user friendly yellow warnings with bloody red exceptions of doom! Use this if you want the function to throw terminating errors you want to catch. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .EXAMPLE PS C:\> Remove-PSMDTemplate -TemplateName '*' -Deprecated Remove all templates that have been superseded by a newer version. .EXAMPLE PS C:\> Get-PSMDTemplate -TemplateName 'module' -RequiredVersion '1.2.2.1' | Remove-PSMDTemplate Removes all copies of the template 'module' with exactly the version '1.2.2.1' #> [CmdletBinding(DefaultParameterSetName = 'NameStore', SupportsShouldProcess = $true, ConfirmImpact = 'High')] Param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'Template')] [PSModuleDevelopment.Template.TemplateInfo[]] $Template, [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'NameStore')] [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'NamePath')] [string] $TemplateName, [Parameter(ParameterSetName = 'NameStore')] [string] $Store = "*", [Parameter(Mandatory = $true, ParameterSetName = 'NamePath')] [string] $Path, [Parameter(ParameterSetName = 'NameStore')] [Parameter(ParameterSetName = 'NamePath')] [switch] $Deprecated, [switch] $EnableException ) begin { $templates = @() switch ($PSCmdlet.ParameterSetName) { 'NameStore' { $templates = Get-PSMDTemplate -TemplateName $TemplateName -Store $Store -All } 'NamePath' { $templates = Get-PSMDTemplate -TemplateName $TemplateName -Path $Path -All } } if ($Deprecated) { $toKill = @() $toKeep = @{ } foreach ($item in $templates) { if ($toKeep.Keys -notcontains $item.Name) { $toKeep[$item.Name] = $item } elseif ($toKeep[$item.Name].Version -lt $item.Version) { $toKill += $toKeep[$item.Name] $toKeep[$item.Name] = $item } else { $toKill += $item} } $templates = $toKill } function Remove-Template { <# .SYNOPSIS Deletes the files associated with a given template. .DESCRIPTION Deletes the files associated with a given template. Takes objects returned by Get-PSMDTemplate. .PARAMETER Template The template to kill. .EXAMPLE PS C:\> Remove-Template -Template $template Removes the template stored in $template #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] Param ( [PSModuleDevelopment.Template.TemplateInfo] $Template ) $pathFile = $Template.Path $pathInfo = $Template.Path -replace '\.xml$', '-Info.xml' Remove-Item $pathInfo -Force -ErrorAction Stop Remove-Item $pathFile -Force -ErrorAction Stop } } process { foreach ($item in $Template) { Invoke-PSFProtectedCommand -ActionString 'Remove-PSMDTemplate.Removing.Template' -Target $item.Name -ScriptBlock { Remove-Template -Template $item } -EnableException $EnableException.ToBool() -PSCmdlet $PSCmdlet -Continue -ActionStringValues $item.Name, $item.Version, $item.Store } foreach ($item in $templates) { Invoke-PSFProtectedCommand -ActionString 'Remove-PSMDTemplate.Removing.Template' -Target $item.Name -ScriptBlock { Remove-Template -Template $item } -EnableException $EnableException.ToBool() -PSCmdlet $PSCmdlet -Continue -ActionStringValues $item.Name, $item.Version, $item.Store } } } function Find-PSMDFileContent { <# .SYNOPSIS Used to quickly search in module files. .DESCRIPTION This function can be used to quickly search files in your module's path. By using Set-PSMDModulePath (or Set-PSFConfig 'PSModuleDevelopment.Module.Path' '<path>') you can set the default path to search in. Using Register-PSFConfig -FullName 'PSModuleDevelopment.Module.Path' allows you to persist this setting across sessions. .PARAMETER Pattern The text to search for, can be any regex pattern .PARAMETER Extension The extension of files to consider. Only files with this extension will be searched. .PARAMETER Path The path to use as search base. Defaults to the path found in the setting 'PSModuleDevelopment.Module.Path' .PARAMETER EnableException Replaces user friendly yellow warnings with bloody red exceptions of doom! Use this if you want the function to throw terminating errors you want to catch. .EXAMPLE PS C:\> Find-PSMDFileContent -Pattern 'Get-Test' Searches all module files for the string 'Get-Test'. #> [Alias('find')] [CmdletBinding()] Param ( [Parameter(Mandatory = $true, Position = 0)] [string] $Pattern, [string] $Extension = (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Find.DefaultExtensions'), [string] $Path = (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Module.Path'), [switch] $EnableException ) begin { if (-not (Test-Path -Path $Path)) { Stop-PSFFunction -Message "Path not found: $Path" -EnableException $EnableException -Category InvalidArgument -Tag "fail", "path", "argument" return } } process { if (Test-PSFFunctionInterrupt) { return } Get-ChildItem -Path $Path -Recurse | Where-Object Extension -Match $Extension | Select-String -Pattern $Pattern } } function Get-PSMDArgumentCompleter { <# .SYNOPSIS Gets the registered argument completers. .DESCRIPTION This function can be used to serach the argument completers registered using either the Register-ArgumentCompleter command or created using the ArgumentCompleter attribute. .PARAMETER CommandName Filter the results to a specific command. Wildcards are supported. .PARAMETER ParameterName Filter results to a specific parameter name. Wildcards are supported. .EXAMPLE PS C:\> Get-PSMDArgumentCompleter Get all argument completers in use in the current PowerShell session. #> [CmdletBinding()] Param ( [Parameter(Position = 1, ValueFromPipeline = $true, ValueFromPipelineByPropertyName)] [Alias('Name')] [String] $CommandName = '*', [String] $ParameterName = '*' ) begin { $internalExecutionContext = [PSFramework.Utility.UtilityHost]::GetExecutionContextFromTLS() $customArgumentCompleters = [PSFramework.Utility.UtilityHost]::GetPrivateProperty('CustomArgumentCompleters', $internalExecutionContext) } process { foreach ($argumentCompleter in $customArgumentCompleters.Keys) { $name, $parameter = $argumentCompleter -split ':' if ($name -like $CommandName) { if ($parameter -like $ParameterName) { [pscustomobject]@{ CommandName = $name ParameterName = $parameter Definition = $customArgumentCompleters[$argumentCompleter] } } } } } } function Measure-PSMDLinesOfCode { <# .SYNOPSIS Measures the lines of code ina PowerShell scriptfile. .DESCRIPTION Measures the lines of code ina PowerShell scriptfile. This scan uses the AST to figure out how many lines contain actual functional code. .PARAMETER Path Path to the files to scan. Folders will be ignored. .EXAMPLE PS C:\> Measure-PSMDLinesOfCode -Path .\script.ps1 Measures the lines of code in the specified file. .EXAMPLE PS C:\> Get-ChildItem C:\Scripts\*.ps1 | Measure-PSMDLinesOfCode Measures the lines of code for every single file in the folder c:\Scripts. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('FullName')] [string[]] $Path ) begin { #region Utility Functions function Invoke-AstWalk { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')] [CmdletBinding()] param ( $Ast, [string[]] $Command, [string[]] $Name, [string] $NewName, [bool] $IsCommand, [bool] $NoAlias, [switch] $First ) #Write-PSFMessage -Level Host -Message "Processing $($Ast.Extent.StartLineNumber) | $($Ast.Extent.File) | $($Ast.GetType().FullName)" $typeName = $Ast.GetType().FullName switch ($typeName) { 'System.Management.Automation.Language.StringConstantExpressionAst' { $Ast.Extent.StartLineNumber .. $Ast.Extent.EndLineNumber } 'System.Management.Automation.Language.IfStatementAst' { $Ast.Extent.StartLineNumber $Ast.Extent.EndLineNumber foreach ($clause in $Ast.Clauses) { Invoke-AstWalk -Ast $clause.Item1 -Command $Command -Name $Name -NewName $NewName -IsCommand $IsCommand Invoke-AstWalk -Ast $clause.Item2 -Command $Command -Name $Name -NewName $NewName -IsCommand $IsCommand } if ($null -ne $Ast.ElseClause) { Invoke-AstWalk -Ast $Ast.ElseClause -Command $Command -Name $Name -NewName $NewName -IsCommand $IsCommand } } default { if (-not $First) { $Ast.Extent.StartLineNumber $Ast.Extent.EndLineNumber } foreach ($property in $Ast.PSObject.Properties) { if ($property.Name -eq "Parent") { continue } if ($null -eq $property.Value) { continue } if (Get-Member -InputObject $property.Value -Name GetEnumerator -MemberType Method) { foreach ($item in $property.Value) { if ($item.PSObject.TypeNames -contains "System.Management.Automation.Language.Ast") { Invoke-AstWalk -Ast $item -Command $Command -Name $Name -NewName $NewName -IsCommand $IsCommand } } continue } if ($property.Value.PSObject.TypeNames -contains "System.Management.Automation.Language.Ast") { Invoke-AstWalk -Ast $property.Value -Command $Command -Name $Name -NewName $NewName -IsCommand $IsCommand } } } } } #endregion Utility Functions } process { #region Process Files foreach ($fileItem in $Path) { Write-PSFMessage -Level VeryVerbose -String MeasurePSMDLinesOfCode.Processing -StringValues $fileItem foreach ($resolvedPath in (Resolve-PSFPath -Path $fileItem -Provider FileSystem)) { if ((Get-Item $resolvedPath).PSIsContainer) { continue } $parsedItem = Read-PSMDScript -Path $resolvedPath $object = New-Object PSModuleDevelopment.Utility.LinesOfCode -Property @{ Path = $resolvedPath } if ($parsedItem.Ast) { $object.Ast = $parsedItem.Ast $object.Lines = Invoke-AstWalk -Ast $parsedItem.Ast -First | Sort-Object -Unique $object.Count = ($object.Lines | Measure-Object).Count $object.Success = $true } $object } } #endregion Process Files } } function New-PSMDHeader { <# .SYNOPSIS Generates a header wrapping around text. .DESCRIPTION Generates a header wrapping around text. The output is an object that contains the configuration options to generate a header. Use its ToString() method (or cast it to string) to generate the header. .PARAMETER Text The text to wrap into a header. Can handle multiline text. When passing a list of strings, each string will be wrapped into its own header. .PARAMETER BorderBottom The border used for the bottom of the frame. Use a single letter, such as "-" .PARAMETER BorderLeft The border used for the left side of the frame. .PARAMETER BorderRight The border used for the right side of the frame. .PARAMETER BorderTop The border used for the top of the frame. Use a single letter, such as "-" .PARAMETER CornerLB The symbol used for the left-bottom corner of the frame .PARAMETER CornerLT The symbol used for the left-top corner of the frame .PARAMETER CornerRB The symbol used for the right-bottom corner of the frame .PARAMETER CornerRT The symbol used for the right-top corner of the frame .PARAMETER MaxWidth Whether to align the frame's total width to the window width. .PARAMETER Padding Whether the text should be padded. Only applies to left/right aligned text. .PARAMETER TextAlignment Default: Center Whether the text should be aligned left, center or right. .PARAMETER Width Total width of the header. Defaults to entire screen. .EXAMPLE PS C:\> New-PSMDHeader -Text 'Example' Will create a header labeled 'Example' that spans the entire screen. .EXAMPLE PS C:\> New-PSMDHeader -Text 'Example' -Width 80 Will create a header labeled 'Example' with a total width of 80: #----------------------------------------------------------------------------# # Example # #----------------------------------------------------------------------------# .EXAMPLE PS C:\> New-PSMDHeader -Text 'Example' -Width 80 -BorderLeft " |" -BorderRight "| " -CornerLB " \" -CornerLT " /" -CornerRB "/" -CornerRT "\" Will create a header labeled "Example with a total width of 80 and some custom border lines: /----------------------------------------------------------------------------\ | Example | \----------------------------------------------------------------------------/ #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] Param ( [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [string[]] $Text, [string] $BorderBottom = "-", [string] $BorderLeft = " #", [string] $BorderRight = "# ", [string] $BorderTop = "-", [string] $CornerLB = " #", [string] $CornerLT = " #", [string] $CornerRB = "# ", [string] $CornerRT = "# ", [switch] $MaxWidth, [int] $Padding = 0, [PSModuleDevelopment.Utility.TextAlignment] $TextAlignment = "Center", [int] $Width = $Host.UI.RawUI.WindowSize.Width ) process { foreach ($line in $Text) { $header = New-Object PSModuleDevelopment.Utility.TextHeader($line) $header.BorderBottom = $BorderBottom $header.BorderLeft = $BorderLeft $header.BorderRight = $BorderRight $header.BorderTop = $BorderTop $header.CornerLB = $CornerLB $header.CornerLT = $CornerLT $header.CornerRB = $CornerRB $header.CornerRT = $CornerRT $header.Padding = $Padding $header.TextAlignment = $TextAlignment if ((Test-PSFParameterBinding -ParameterName Width) -and (Test-PSFParameterBinding -ParameterName MaxWidth -Not)) { $header.MaxWidth = $false $header.Width = $Width } else { $header.MaxWidth = $MaxWidth $header.Width = $Width } $header } } } function New-PSMDModuleNugetPackage { <# .SYNOPSIS Creates a nuget package from a PowerShell module. .DESCRIPTION This function will take a module and wrap it into a nuget package. This is accomplished by creating a temporary local filesystem repository and using the PowerShellGet module to do the actual writing. Note: - Requires PowerShellGet module - Dependencies must be built first to the same folder .PARAMETER ModulePath Path to the PowerShell module you are creating a Nuget package from .PARAMETER PackagePath Path where the package file will be copied. .PARAMETER EnableException Replaces user friendly yellow warnings with bloody red exceptions of doom! Use this if you want the function to throw terminating errors you want to catch. .EXAMPLE New-PSMDModuleNugetPackage -PackagePath 'c:\temp\package' -ModulePath .\DBOps Packages the module stored in .\DBOps and stores the nuget file in 'c:\temp\package' .NOTES Author: Mark Wilkinson Editor: Friedrich Weinmann #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [Parameter(mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('ModuleBase')] [string[]] $ModulePath, [string] $PackagePath = (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Package.Path' -Fallback "$env:TEMP"), [switch] $EnableException ) begin { #region Input validation and prerequisites check try { $null = Get-Command Publish-Module -ErrorAction Stop $null = Get-Command Register-PSRepository -ErrorAction Stop $null = Get-Command Unregister-PSRepository -ErrorAction Stop } catch { $paramStopPSFFunction = @{ Message = "Failed to detect the PowerShellGet module! The module is required in order to execute this function." EnableException = $EnableException Category = 'NotInstalled' ErrorRecord = $_ OverrideExceptionMessage = $true Tag = 'fail', 'validation', 'prerequisites', 'module' } Stop-PSFFunction @paramStopPSFFunction return } if (-not (Test-Path $PackagePath)) { Write-PSFMessage -Level Verbose -Message "Creating path: $PackagePath" -Tag 'begin', 'create', 'path' try { $null = New-Item -Path $PackagePath -ItemType Directory -Force -ErrorAction Stop } catch { Stop-PSFFunction -Message "Failed to create output path: $PackagePath" -ErrorRecord $_ -EnableException $EnableException -Tag 'fail', 'bgin', 'create', 'path' return } } $resolvedPath = (Get-Item -Path $PackagePath).FullName #endregion Input validation and prerequisites check #region Prepare local Repository try { if (Get-PSRepository | Where-Object Name -EQ 'PSModuleDevelopment_TempLocalRepository') { Unregister-PSRepository -Name 'PSModuleDevelopment_TempLocalRepository' } $paramRegisterPSRepository = @{ Name = 'PSModuleDevelopment_TempLocalRepository' PublishLocation = $resolvedPath SourceLocation = $resolvedPath InstallationPolicy = 'Trusted' ErrorAction = 'Stop' } Register-PSRepository @paramRegisterPSRepository } catch { Stop-PSFFunction -Message "Failed to create temporary PowerShell Repository" -ErrorRecord $_ -EnableException $EnableException -Tag 'fail', 'bgin', 'create', 'path' return } #endregion Prepare local Repository } process { if (Test-PSFFunctionInterrupt) { return } #region Process Paths foreach ($Path in $ModulePath) { Write-PSFMessage -Level VeryVerbose -Message "Starting to package: $Path" -Tag 'progress', 'developer' -Target $Path if (-not (Test-Path $Path)) { Stop-PSFFunction -Message "Path not found: $Path" -EnableException $EnableException -Category InvalidArgument -Tag 'progress', 'developer', 'fail' -Target $Path -Continue } try { Publish-Module -Path $Path -Repository 'PSModuleDevelopment_TempLocalRepository' -ErrorAction Stop -Force } catch { Stop-PSFFunction -Message "Failed to publish module: $Path" -EnableException $EnableException -ErrorRecord $_ -Tag 'progress', 'developer', 'fail' -Target $Path -Continue } Write-PSFMessage -Level Verbose -Message "Finished processing: $Path" -Tag 'progress', 'developer' -Target $Path } #endregion Process Paths } end { Unregister-PSRepository -Name 'PSModuleDevelopment_TempLocalRepository' -ErrorAction Ignore if (Test-PSFFunctionInterrupt) { return } } } function New-PssModuleProject { <# .SYNOPSIS Builds a Sapien PowerShell Studio Module Project from a regular module. .DESCRIPTION Builds a Sapien PowerShell Studio Module Project, either a clean one, or imports from a regular module. Will ignore all hidden files and folders, will also ignore all files and folders in the root folder that start with a dot ("."). Importing from an existing module requires the module to have a valid manifest. .PARAMETER Name The name of the folder to create the project in. Will also be used to name a blank module project. (When importing a module into a project, the name will be taken from the manifest file). .PARAMETER Path The path to create the new module-project folder in. Will default to the PowerShell Studio project folder. The function will fail if PSS is not found on the system and no path was specified. .PARAMETER SourcePath The path to the module to import from. Specify the path the the root folder the actual module files are in. .PARAMETER Force Force causes the function to overwrite all stuff in the destination folder ($Path\$Name), if it already exists. .EXAMPLE PS C:\> New-PssModuleProject -Name 'Foo' Creates a new module project named "Foo" in your default project folder. .EXAMPLE PS C:\> New-PssModuleProject -Name dbatools -SourcePath "C:\Github\dbatools" Imports the dbatools github repo's local copy into a new PSS module project in your default project folder. .EXAMPLE PS C:\> New-PssModuleProject -name 'Northwind' -SourcePath "C:\Github\Northwind" -Path "C:\Projects" -Force Will create a new module project, importing from "C:\Github\Northwind" and storing it in "C:\Projects". It will overwrite any existing folder named "Northwind" in the destination folder. .NOTES Author: Friedrich Weinmann Editors: - Created on: 01.03.2017 Last Change: 01.03.2017 Version: 1.0 Release 1.0 (01.03.2017, Friedrich Weinmann) - Initial Release #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding(DefaultParameterSetName = "Vanilla")] Param ( [Parameter(Mandatory = $true)] [string] $Name, [ValidateScript({ Test-Path -Path $_ -PathType Container })] [string] $Path, [Parameter(Mandatory = $true, ParameterSetName = "Import")] [string] $SourcePath, [switch] $Force ) if (Test-PSFParameterBinding -ParameterName "Path" -Not) { try { $pssRoot = (Get-ChildItem "HKCU:\Software\SAPIEN Technologies, Inc." -ErrorAction Stop | Where-Object Name -like "*PowerShell Studio*" | Select-Object -last 1 -ExpandProperty Name).Replace("HKEY_CURRENT_USER", "HKCU:") $Path = (Get-ItemProperty -Path "$pssRoot\Settings" -Name "DefaultProjectDirectory" -ErrorAction Stop).DefaultProjectDirectory } catch { throw "No local PowerShell Studio found and no path specified. Going to take a break now. Bye!" } } switch ($PSCmdlet.ParameterSetName) { #region Vanilla "Vanilla" { if ((-not $Force) -and (Test-Path (Join-Path $Path $Name))) { throw "There already is an existing folder in '$Path\$Name', cannot create module!" } $root = New-Item -Path $Path -Name $Name -ItemType Directory -Force:$Force $Guid = [guid]::NewGuid().Guid # Create empty .psm1 file Set-Content -Path "$($root.FullName)\$Name.psm1" -Value "" #region Create Manifest Set-Content -Path "$($root.FullName)\$Name.psd1" -Value @" @{ # Script module or binary module file associated with this manifest ModuleToProcess = '$Name.psm1' # Version number of this module. ModuleVersion = '1.0.0.0' # ID used to uniquely identify this module GUID = '$Guid' # Author of this module Author = '' # Company or vendor of this module CompanyName = '' # Copyright statement for this module Copyright = '(c) $((Get-Date).Year). All rights reserved.' # Description of the functionality provided by this module Description = 'Module description' # Minimum version of the Windows PowerShell engine required by this module PowerShellVersion = '2.0' # Name of the Windows PowerShell host required by this module PowerShellHostName = '' # Minimum version of the Windows PowerShell host required by this module PowerShellHostVersion = '' # Minimum version of the .NET Framework required by this module DotNetFrameworkVersion = '2.0' # Minimum version of the common language runtime (CLR) required by this module CLRVersion = '2.0.50727' # Processor architecture (None, X86, Amd64, IA64) required by this module ProcessorArchitecture = 'None' # Modules that must be imported into the global environment prior to importing # this module RequiredModules = @() # Assemblies that must be loaded prior to importing this module RequiredAssemblies = @() # Script files (.ps1) that are run in the caller's environment prior to # importing this module ScriptsToProcess = @() # Type files (.ps1xml) to be loaded when importing this module TypesToProcess = @() # Format files (.ps1xml) to be loaded when importing this module FormatsToProcess = @() # Modules to import as nested modules of the module specified in # ModuleToProcess NestedModules = @() # Functions to export from this module FunctionsToExport = '*' #For performanace, list functions explicity # Cmdlets to export from this module CmdletsToExport = '*' # Variables to export from this module VariablesToExport = '*' # Aliases to export from this module AliasesToExport = '*' #For performanace, list alias explicity # List of all modules packaged with this module ModuleList = @() # List of all files packaged with this module FileList = @() # Private data to pass to the module specified in ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. PrivateData = @{ #Support for PowerShellGet galleries. PSData = @{ # Tags applied to this module. These help with module discovery in online galleries. # Tags = @() # A URL to the license for this module. # LicenseUri = '' # A URL to the main website for this project. # ProjectUri = '' # A URL to an icon representing this module. # IconUri = '' # ReleaseNotes of this module # ReleaseNotes = '' } # End of PSData hashtable } # End of PrivateData hashtable } "@ #endregion Create Manifest #region Create project file Set-Content -Path "$($root.FullName)\$Name.psproj" -Value @" <Project> <Version>2.0</Version> <FileID>$Guid</FileID> <ProjectType>1</ProjectType> <Folders /> <Files> <File Build="2">$Name.psd1</File> <File Build="0">$Name.psm1</File> </Files> </Project> "@ #endregion Create project file } #endregion Vanilla #region Import "Import" { $SourcePath = Resolve-Path $SourcePath if (-not (Test-Path $SourcePath)) { throw "Source path was not detectable!" } if ((-not $Force) -and (Test-Path (Join-Path $Path $Name))) { throw "There already is an existing folder in '$Path\$Name', cannot create module!" } $items = Get-ChildItem -Path $SourcePath | Where-Object Name -NotLike ".*" $root = New-Item -Path $Path -Name $Name -ItemType Directory -Force:$Force $items | Copy-Item -Destination $root.FullName -Recurse -Force $items_directories = Get-ChildItem -Path $root.FullName -Recurse -Directory $items_psd = Get-Item "$($root.FullName)\*.psd1" | Select-Object -First 1 if (-not $items_psd) { throw "no module manifest found!" } $ModuleName = $items_psd.BaseName $items_files = Get-ChildItem -Path $root.FullName -Recurse -File | Where-Object { ($_.FullName -ne $items_psd.FullName) -and ($_.FullName -ne $items_psd.FullName.Replace(".psd1",".psm1")) } $Guid = (Get-Content $items_psd.FullName | Select-String "GUID = '(.+?)'").Matches[0].Groups[1].Value $string_Files = ($items_files | Select-Object -ExpandProperty FullName | ForEach-Object { " <File Build=`"2`" Shared=`"True`">$(($_ -replace ([regex]::Escape(($root.FullName + "\"))), ''))</File>" }) -join "`n" $string_Directories = ($items_Directories | Select-Object -ExpandProperty FullName | ForEach-Object { " <Folder>$(($_ -replace ([regex]::Escape(($root.FullName + "\"))), ''))</Folder>" }) -join "`n" Set-Content -Path "$($root.FullName)\$ModuleName.psproj" -Value @" <Project> <Version>2.0</Version> <FileID>$Guid</FileID> <ProjectType>1</ProjectType> <Folders> $($string_Directories) </Folders> <Files> <File Build="2">$ModuleName.psd1</File> <File Build="0">$ModuleName.psm1</File> $($string_Files) </Files> </Project> "@ } #endregion Import } } function Restart-PSMDShell { <# .SYNOPSIS A swift way to restart the PowerShell console. .DESCRIPTION A swift way to restart the PowerShell console. - Allows increasing elevation - Allows keeping the current process, thus in effect adding a new PowerShell process .PARAMETER NoExit The current console will not terminate. .PARAMETER Admin The new PowerShell process will be run as admin. .PARAMETER NoProfile The new PowerShell process will not load its profile. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .EXAMPLE PS C:\> Restart-PSMDShell Restarts the current PowerShell process. .EXAMPLE PS C:\> Restart-PSMDShell -Admin -NoExit Creates a new PowerShell process, run with elevation, while keeping the current console around. #> [Alias('rss', 'Restart-Shell')] [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low')] Param ( [Switch] $NoExit, [Switch] $Admin, [switch] $NoProfile ) begin { $process = Get-Process -Id $pid $powershellPath = $process.Path $isWindowsTerminal = $process.Parent.ProcessName -eq 'WindowsTerminal' } process { if (-not $PSCmdlet.ShouldProcess("Current shell", "Restart")) { return } if ($isWindowsTerminal) { $psVersionName = 'powershell' if ($PSVersionTable.PSVersion.Major -gt 5) { $psVersionName = 'pwsh' } $param = @{ FilePath = 'wt' ArgumentList = @('-w', 0, 'nt','--title', $psVersionName, $powershellPath) } if ($NoProfile) { $param.ArgumentList = @('-w', 0, 'nt', '--title', $psVersionName, $powershellPath, '-NoProfile') } if ($Admin) { $param.Verb = 'RunAs' } Start-Process @param } else { $param = @{ FilePath = $powershellPath } if ($NoProfile) { $param.ArgumentList = '-NoProfile' } if ($Admin) { $param.Verb = 'RunAs' } Start-Process @param } } end { if (-not $NoExit) { exit } } } function Search-PSMDPropertyValue { <# .SYNOPSIS Recursively search an object for property values. .DESCRIPTION Recursively search an object for property values. This can be useful to determine just where an object stores a given piece of information in scenarios, where objects either have way too many properties or a deeply nested data structure. .PARAMETER Object The object to search. .PARAMETER Value The value to search for. .PARAMETER Match Search by comparing with regex, rather than equality comparison. .PARAMETER Depth Default: 3 How deep should the query recurse. The deeper, the longer it can take on deeply nested objects. .EXAMPLE PS C:\> Get-Mailbox Max.Mustermann | Search-PSMDPropertyValue -Object 'max.mustermann@contoso.com' -Match Searches all properties on the mailbox of Max Mustermann for his email address. #> [CmdletBinding()] param ( [AllowNull()] $Value, [Parameter(ValueFromPipeline = $true, Mandatory = $true)] $Object, [switch] $Match, [int] $Depth = 3 ) begin { function Search-Value { [CmdletBinding()] param ( $Object, $Value, [bool] $Match, [int] $Depth, [string[]] $Elements, $InputObject ) $path = $Elements -join "." Write-PSFMessage -Level Verbose -Message "Processing $path" foreach ($property in $Object.PSObject.Properties) { if ($Match) { if ($property.Value -match $Value) { New-Object PSModuleDevelopment.Utility.PropertySearchResult($property.Name, $Elements, $property.Value, $InputObject) } } else { if ($Value -eq $property.Value) { New-Object PSModuleDevelopment.Utility.PropertySearchResult($property.Name, $Elements, $property.Value, $InputObject) } } if ($Elements.Count -lt $Depth) { $newItems = New-Object System.Object[]($Elements.Count) $Elements.CopyTo($newItems, 0) $newItems += $property.Name Search-Value -Object $property.Value -Value $Value -Match $Match -Depth $Depth -Elements $newItems -InputObject $InputObject } } } } process { Search-Value -Object $Object -Value $Value -Match $Match.ToBool() -Depth $Depth -Elements @() -InputObject $Object } } function Set-PSMDModulePath { <# .SYNOPSIS Sets the path of the module currently being developed. .DESCRIPTION Sets the path of the module currently being developed. This is used by several utility commands in order to not require any path input. This is a wrapper around the psframework configuration system, the same action can be taken by running this command: Set-PSFConfig -Module PSModuleDevelopment -Name "Module.Path" -Value $Path .PARAMETER Module The module, the path of which to register. .PARAMETER Path The path to set as currently developed module. .PARAMETER Register Register the specified path, to have it persist across sessions .PARAMETER EnableException Replaces user friendly yellow warnings with bloody red exceptions of doom! Use this if you want the function to throw terminating errors you want to catch. .EXAMPLE Set-PSMDModulePath -Path "C:\github\dbatools" Sets the current module path to "C:\github\dbatools" .EXAMPLE Set-PSMDModulePath -Path "C:\github\dbatools" -Register Sets the current module path to "C:\github\dbatools" Then stores the setting in registry, causing it to be persisted acros multiple sessions. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'Module')] [System.Management.Automation.PSModuleInfo] $Module, [Parameter(Mandatory = $true, ParameterSetName = 'Path')] [string] $Path, [switch] $Register, [switch] $EnableException ) process { if ($Path) { $resolvedPath = Resolve-PSFPath -Path $Path -Provider FileSystem -SingleItem if (Test-Path -Path $resolvedPath) { if ((Get-Item $resolvedPath).PSIsContainer) { Set-PSFConfig -Module PSModuleDevelopment -Name "Module.Path" -Value $resolvedPath if ($Register) { Register-PSFConfig -Module 'PSModuleDevelopment' -Name 'Module.Path' } return } } Stop-PSFFunction -Target $Path -Message "Could not validate/resolve path: $Path" -EnableException $EnableException -Category InvalidArgument return } else { Set-PSFConfig -Module PSModuleDevelopment -Name "Module.Path" -Value $Module.ModuleBase if ($Register) { Register-PSFConfig -Module 'PSModuleDevelopment' -Name 'Module.Path' } } } } function Show-PSMDSyntax { <# .SYNOPSIS Validate or show parameter set details with colored output .DESCRIPTION Analyze a function and it's parameters The cmdlet / function is capable of validating a string input with function name and parameters .PARAMETER CommandText The string that you want to analyze If there is parameter value present, you have to use the opposite quote strategy to encapsulate the string correctly E.g. for double quotes -CommandText 'New-Item -Path "c:\temp\newfile.txt"' E.g. for single quotes -CommandText "New-Item -Path 'c:\temp\newfile.txt'" .PARAMETER Mode The operation mode of the cmdlet / function Valid options are: - Validate - ShowParameters .PARAMETER Legend Include a legend explaining the color mapping .EXAMPLE PS C:\> Show-PSMDSyntax -CommandText "New-Item -Path 'c:\temp\newfile.txt'" This will validate all the parameters that have been passed to the Import-D365Bacpac cmdlet. All supplied parameters that matches a parameter will be marked with an asterisk. .EXAMPLE PS C:\> Show-PSMDSyntax -CommandText "New-Item" -Mode "ShowParameters" This will display all the parameter sets and their individual parameters. .NOTES Author: Mötz Jensen (@Splaxi) Twitter: https://twitter.com/splaxi Original github project: https://github.com/d365collaborative/d365fo.tools #> [CmdletBinding()] param ( [Parameter(Mandatory = $true, Position = 1)] [string] $CommandText, [Parameter(Position = 2)] [ValidateSet('Validate', 'ShowParameters')] [string] $Mode = 'Validate', [switch] $Legend ) $commonParameters = 'Verbose', 'Debug', 'ErrorAction', 'WarningAction', 'InformationAction', 'ErrorVariable', 'WarningVariable', 'InformationVariable', 'OutVariable', 'OutBuffer', 'PipelineVariable', 'Confirm', 'WhatIf' $colorParmsNotFound = Get-PSFConfigValue -FullName "PSModuleDevelopment.ShowSyntax.ParmsNotFound" $colorCommandName = Get-PSFConfigValue -FullName "PSModuleDevelopment.ShowSyntax.CommandName" $colorMandatoryParam = Get-PSFConfigValue -FullName "PSModuleDevelopment.ShowSyntax.MandatoryParam" $colorNonMandatoryParam = Get-PSFConfigValue -FullName "PSModuleDevelopment.ShowSyntax.NonMandatoryParam" $colorFoundAsterisk = Get-PSFConfigValue -FullName "PSModuleDevelopment.ShowSyntax.FoundAsterisk" $colorNotFoundAsterisk = Get-PSFConfigValue -FullName "PSModuleDevelopment.ShowSyntax.NotFoundAsterisk" $colParmValue = Get-PSFConfigValue -FullName "PSModuleDevelopment.ShowSyntax.ParmValue" #Match to find the command name: Non-Whitespace until the first whitespace $commandMatch = ($CommandText | Select-String '\S+\s*').Matches if (-not $commandMatch) { Write-PSFMessage -Level Host -Message "The function was unable to extract a valid command name from the supplied command text. Please try again." Stop-PSFFunction -Message "Stopping because of missing command name." return } $commandName = $commandMatch.Value.Trim() $res = Get-Command $commandName -ErrorAction Ignore if (-not $res) { Write-PSFMessage -Level Host -Message "The function was unable to get the help of the command. Make sure that the command name is valid and try again." Stop-PSFFunction -Message "Stopping because command name didn't return any help." return } $sbHelp = New-Object System.Text.StringBuilder $sbParmsNotFound = New-Object System.Text.StringBuilder if (-not ($CommandText | Select-String '\s{1}[-]\S+' -AllMatches).Matches) { $Mode = 'ShowParameters' } switch ($Mode) { "Validate" { # Match to find the parameters: Whitespace Dash Non-Whitespace $inputParameterMatch = ($CommandText | Select-String '\s{1}[-]\S+' -AllMatches).Matches if ($inputParameterMatch) { $inputParameterNames = $inputParameterMatch.Value.Trim("-", " ") Write-PSFMessage -Level Verbose -Message "All input parameters - $($inputParameterNames -join ",")" -Target ($inputParameterNames -join ",") } else { Write-PSFMessage -Level Host -Message "The function was unable to extract any parameters from the supplied command text. Please try again." Stop-PSFFunction -Message "Stopping because of missing input parameters." return } $availableParameterNames = (Get-Command $commandName).Parameters.keys | Where-Object { $commonParameters -NotContains $_ } Write-PSFMessage -Level Verbose -Message "Available parameters - $($availableParameterNames -join ",")" -Target ($availableParameterNames -join ",") $inputParameterNotFound = $inputParameterNames | Where-Object { $availableParameterNames -NotContains $_ } if ($inputParameterNotFound.Length -gt 0) { $null = $sbParmsNotFound.AppendLine("Parameters that <c='em'>don't exists</c>") $inputParameterNotFound | ForEach-Object { $null = $sbParmsNotFound.AppendLine("<c='$colorParmsNotFound'>$($_)</c>") } } foreach ($parmSet in (Get-Command $commandName).ParameterSets) { $sb = New-Object System.Text.StringBuilder $null = $sb.AppendLine("ParameterSet Name: <c='em'>$($parmSet.Name)</c> - Validated List") $null = $sb.Append("<c='$colorCommandName'>$commandName </c>") $parmSetParameters = $parmSet.Parameters | Where-Object name -NotIn $commonParameters foreach ($parameter in $parmSetParameters) { $parmFoundInCommandText = $parameter.Name -In $inputParameterNames $color = "$colorNonMandatoryParam" if ($parameter.IsMandatory -eq $true) { $color = "$colorMandatoryParam" } $null = $sb.Append("<c='$color'>-$($parameter.Name)</c>") if ($parmFoundInCommandText) { $null = $sb.Append("<c='$colorFoundAsterisk'>* </c>") } elseif ($parameter.IsMandatory -eq $true) { $null = $sb.Append("<c='$colorNotFoundAsterisk'>* </c>") } else { $null = $sb.Append(" ") } if (-not ($parameter.ParameterType -eq [System.Management.Automation.SwitchParameter])) { $null = $sb.Append("<c='$colParmValue'>PARAMVALUE </c>") } } $null = $sb.AppendLine("") Write-PSFHostColor -String "$($sb.ToString())" } $null = $sbHelp.AppendLine("") $null = $sbHelp.AppendLine("<c='$colorParmsNotFound'>$colorParmsNotFound</c> = Parameter not found") $null = $sbHelp.AppendLine("<c='$colorCommandName'>$colorCommandName</c> = Command Name") $null = $sbHelp.AppendLine("<c='$colorMandatoryParam'>$colorMandatoryParam</c> = Mandatory Parameter") $null = $sbHelp.AppendLine("<c='$colorNonMandatoryParam'>$colorNonMandatoryParam</c> = Optional Parameter") $null = $sbHelp.AppendLine("<c='$colParmValue'>$colParmValue</c> = Parameter value") $null = $sbHelp.AppendLine("<c='$colorFoundAsterisk'>*</c> = Parameter was filled") $null = $sbHelp.AppendLine("<c='$colorNotFoundAsterisk'>*</c> = Mandatory missing") } "ShowParameters" { foreach ($parmSet in (Get-Command $commandName).ParameterSets) { # (Get-Command $commandName).ParameterSets | ForEach-Object { $sb = New-Object System.Text.StringBuilder $null = $sb.AppendLine("ParameterSet Name: <c='em'>$($parmSet.Name)</c> - Parameter List") $null = $sb.Append("<c='$colorCommandName'>$commandName </c>") $parmSetParameters = $parmSet.Parameters | Where-Object name -NotIn $commonParameters foreach ($parameter in $parmSetParameters) { # $parmSetParameters | ForEach-Object { $color = "$colorNonMandatoryParam" if ($parameter.IsMandatory -eq $true) { $color = "$colorMandatoryParam" } $null = $sb.Append("<c='$color'>-$($parameter.Name) </c>") if (-not ($parameter.ParameterType -eq [System.Management.Automation.SwitchParameter])) { $null = $sb.Append("<c='$colParmValue'>PARAMVALUE </c>") } } $null = $sb.AppendLine("") Write-PSFHostColor -String "$($sb.ToString())" } $null = $sbHelp.AppendLine("") $null = $sbHelp.AppendLine("<c='$colorCommandName'>$colorCommandName</c> = Command Name") $null = $sbHelp.AppendLine("<c='$colorMandatoryParam'>$colorMandatoryParam</c> = Mandatory Parameter") $null = $sbHelp.AppendLine("<c='$colorNonMandatoryParam'>$colorNonMandatoryParam</c> = Optional Parameter") $null = $sbHelp.AppendLine("<c='$colParmValue'>$colParmValue</c> = Parameter value") } Default { } } if ($sbParmsNotFound.ToString().Trim().Length -gt 0) { Write-PSFHostColor -String "$($sbParmsNotFound.ToString())" } if ($Legend) { Write-PSFHostColor -String "$($sbHelp.ToString())" } } function Test-PSMDClmCompatibility { <# .SYNOPSIS Tests, whether the targeted file would have trouble executing under Constrained Language Mode. .DESCRIPTION Tests, whether the targeted file would have trouble executing under Constrained Language Mode (CLM). In CLM, various language features and commands are constrained in their ability to execute. This command uses the AST parser to scan for as many known issues as possible and gives a comprehensive report for concerns found. Detected Issues: - Custom Object creation using PSCustomObject - Calling methods on untrusted types - Converting to an untrusted type - Using Add-Type to load anything but trusted libraries - Using New-Object to instantiate an untrusted type - Assigning Values to properties* *This detection will likely have a large rate of false positives, due to inability to detect datatype of the object, the property of which is being set. Generally, assigning values to the properties of PSObjects is fine. Note: Many of the detections make allowances for "whitelisted types". In CLM, access to most types is constrained, except for a few, known to be trustworthy types. To get a full list of the constraints and what types are allowed, see the documentation: https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_language_modes?view=powershell-7.1#constrained-language-constrained-language .PARAMETER Path Path to the scriptfile to scan. .EXAMPLE PS C:\> Get-ChildItem C:\Scripts | Test-PSMDClmCompatibility Scans each file in C:\Scripts and returns any issues that might occur in CLM. .LINK https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_language_modes?view=powershell-7.1#constrained-language-constrained-language #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')] [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('FullName')] [string[]] $Path ) begin { #region Safe Types $safeTypes = @( [System.Array] [System.Boolean] [System.Byte] [System.Char] [System.DateTime] [System.Decimal] [System.Double] [System.Single] [System.Guid] [System.Collections.Hashtable] [System.Int32] [System.Int16] [System.Int64] [System.Management.Automation.Language.NullString] [System.Management.Automation.PSCredential] [System.Management.Automation.PSListModifier] [System.Management.Automation.PSObject] [System.Management.Automation.PSPrimitiveDictionary] [System.Management.Automation.PSTypeNameAttribute] [System.Text.RegularExpressions.Regex] [System.SByte] [System.String] [System.Globalization.CultureInfo] [System.Net.IPAddress] [System.Net.Mail.MailAddress] [System.Numerics.BigInteger] [System.Security.SecureString] [System.TimeSpan] [System.UInt16] [System.UInt32] [System.UInt64] [System.Management.Automation.AliasAttribute] [System.Management.Automation.AllowEmptyCollectionAttribute] [System.Management.Automation.AllowEmptyStringAttribute] [System.Management.Automation.AllowNullAttribute] [System.Management.Automation.CmdletBindingAttribute] [System.DirectoryServices.DirectoryEntry] [System.DirectoryServices.DirectorySearcher] [System.Management.ManagementClass] [System.Management.ManagementObject] [System.Management.ManagementObjectSearcher] [System.Management.Automation.OutputTypeAttribute] [System.Management.Automation.ParameterAttribute] [System.Management.Automation.PSDefaultValueAttribute] [System.Management.Automation.PSReference] [System.Management.Automation.SupportsWildcardsAttribute] [System.Management.Automation.SwitchParameter] ) #endregion Safe Types #region Utility Functions function Search-Ast { [CmdletBinding()] param ( [System.Management.Automation.Language.Ast] $Ast, [ScriptBlock] $Filter, [string] $Type, [string] $Explanation ) $results = $Ast.FindAll($Filter, $true) foreach ($result in $results) { [PSCustomObject]@{ Type = $Type Line = $result.Extent.StartLineNumber File = $Ast.Extent.File Data = $result Explanation = $Explanation } } } function Format-Result { [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true)] $Result ) begin { $defaultDisplaySet = 'Type', 'Line', 'File', 'Data' $defaultDisplayPropertySet = New-Object System.Management.Automation.PSPropertySet(‘DefaultDisplayPropertySet’, [string[]]$defaultDisplaySet) $standardMembers = [System.Management.Automation.PSMemberInfo[]]@($defaultDisplayPropertySet) } process { $Result | Add-Member -MemberType MemberSet -Name PSStandardMembers -Value $standardMembers -PassThru } } function Find-PSCustomObject { [CmdletBinding()] param ( [System.Management.Automation.Language.Ast] $Ast, $SafeTypes ) $explanation = 'Custom object creation using PSCustomObject is not available in CLM. You can work around this issue by replacing it with "New-Object PSObject -Properties @{ ... }", which works in CLM.' Search-Ast -Ast $Ast -Type PSCustomObject -Explanation $explanation -Filter { if ($args[0] -isnot [System.Management.Automation.Language.ConvertExpressionAst]) { return } if ($args[0].Type.TypeName.Name -ne 'PSCustomObject') { return } $true } | Format-Result } function Find-MethodInvocation { [CmdletBinding()] param ( [System.Management.Automation.Language.Ast] $Ast, $SafeTypes ) $explanation = 'Cannot call methods on objects in CLM, other than ToString, unless the type is one of the few basic trusted types such as string or integer' Search-Ast -Ast $Ast -Type 'Method Invocation' -Explanation $explanation -Filter { if ($args[0] -isnot [System.Management.Automation.Language.InvokeMemberExpressionAst]) { return } if ($args[0].Expression.StaticType -in $SafeTypes) { return } if ($args[0].Member.Value -eq 'ToString') { return } $true } | Format-Result } function Find-TypeConversion { [CmdletBinding()] param ( [System.Management.Automation.Language.Ast] $Ast, $SafeTypes ) $explanation = 'Cannot convert to types not trusted in CLM. Trusted types are few, including very simple types such as string or integer.' Search-Ast -Ast $Ast -Type 'Type Conversion' -Explanation $explanation -Filter { if ($args[0] -isnot [System.Management.Automation.Language.ConvertExpressionAst]) { return } if ($args[0].StaticType -in $SafeTypes) { return } if ($args[0].Type.TypeName.Name -eq 'PSCustomObject' -and $args[0].Child.StaticType -eq [hashtable]) { return } $true } | Format-Result } function Find-AddType { [CmdletBinding()] param ( [System.Management.Automation.Language.Ast] $Ast, $SafeTypes ) $explanation = 'Add-Type can only load signed and trusted libraries. This includes core .NET assemblies loaded by name. If loading an assembly from file, this detection will trigger, as it does not verify the file referenced. If the targeted dll is signed and trusted, disregard this detection.' Search-Ast -Ast $Ast -Type 'Add-Type' -Explanation $explanation -Filter { if ($args[0] -isnot [System.Management.Automation.Language.CommandAst]) { return } if ($args[0].CommandElements[0].Value -ne 'Add-Type') { return } if ($args[0].CommandElements.ParameterName -contains 'AssemblyName') { return } $true } | Format-Result } function Find-NewObject { [CmdletBinding()] param ( [System.Management.Automation.Language.Ast] $Ast, $SafeTypes ) $explanation = 'New-Object cannot be used in CLM, except to create an object of one of a set of explicitly whitelisted types, such as strings, integers, DateTime, etc.' Search-Ast -Ast $Ast -Type 'New-Object' -Explanation $explanation -Filter { if ($args[0] -isnot [System.Management.Automation.Language.CommandAst]) { return } if ($args[0].CommandElements[0].Value -ne 'New-Object') { return } if ($args[0].CommandElements | Where-Object Value -In $SafeTypes.FullName) { return } if ($args[0].CommandElements | Where-Object Value -eq 'PSObject') { return } $true } | Format-Result } function Find-PropertyAssignment { [CmdletBinding()] param ( [System.Management.Automation.Language.Ast] $Ast, $SafeTypes ) $explanation = 'Under CLM, assigning values to properties doesn''t work, unless the type is explicitly whitelisted by the engine. Generic PSObject objects - such as returned by ConvertFrom-Json or Import-Csv - ARE whitelisted however, so this scan may have a few false positives, sorry.' Search-Ast -Ast $Ast -Type 'Property Assignment' -Explanation $explanation -Filter { if ($args[0] -isnot [System.Management.Automation.Language.AssignmentStatementAst]) { return } if ($args[0].Left -isnot [System.Management.Automation.Language.MemberExpressionAst]) { return } if ($args[0].CommandElements | Where-Object Value -In $SafeTypes.FullName) { return } $true } | Format-Result } function Find-ClassDefinition { [CmdletBinding()] param ( [System.Management.Automation.Language.Ast] $Ast, $SafeTypes ) $explanation = 'PowerShell classes are not supported in Constrained Language Mode.' Search-Ast -Ast $Ast -Type 'PowerShell Class' -Explanation $explanation -Filter { if ($args[0] -isnot [System.Management.Automation.Language.TypeDefinitionAst]) { return } if ($args[0].TypeAttributes -eq 'Enum') { return } $true } | Format-Result } #endregion Utility Functions } process { foreach ($file in ($Path | Resolve-Path).Path) { try { $ast = [System.Management.Automation.Language.Parser]::ParseFile($file, [ref]$null, [ref]$null) } catch { Write-PSFMessage -Level Warning -Message "Error parsing: $file" -ErrorRecord $_ -PSCmdlet $PSCmdlet -EnableException $true continue } $param = @{ Ast = $ast SafeTypes = $safeTypes } Find-PSCustomObject @param Find-MethodInvocation @param Find-TypeConversion @param Find-AddType @param Find-NewObject @param Find-PropertyAssignment @param Find-ClassDefinition @param } } } Set-PSFScriptblock -Name PSModuleDevelopment.Validate.Path -Scriptblock { Test-Path $_ } Set-PSFScriptblock -Name PSModuleDevelopment.Validate.File -Scriptblock { Test-Path $_ -PathType Leaf } Register-PSFTeppScriptblock -Name 'PSModuleDevelopment.Build.Action' -ScriptBlock { (Get-PSMDBuildAction).Name } Register-PSFTeppScriptblock -Name PSMD_dotNetTemplates -ScriptBlock { if (-not (Test-Path "$env:USERPROFILE\.templateengine\dotnetcli")) { return } $folder = (Get-ChildItem "$env:USERPROFILE\.templateengine\dotnetcli" | Sort-Object Name | Select-Object -Last 1).FullName Get-Content -Path "$folder\templatecache.json" | ConvertFrom-Json | Select-Object -ExpandProperty TemplateInfo | Select-Object -ExpandProperty ShortName -Unique } Register-PSFTeppScriptblock -Name PSMD_dotNetTemplatesInstall -ScriptBlock { Get-PSFTaskEngineCache -Module PSModuleDevelopment -Name "dotNetTemplates" } Register-PSFTeppScriptblock -Name PSMD_dotNetTemplatesUninstall -ScriptBlock { if (-not (Test-Path "$env:USERPROFILE\.templateengine\dotnetcli")) { return } $folder = (Get-ChildItem "$env:USERPROFILE\.templateengine\dotnetcli" | Sort-Object Name | Select-Object -Last 1).FullName $items = Get-Content -Path "$folder\installUnitDescriptors.json" | ConvertFrom-Json | Select-Object -ExpandProperty InstalledItems $items.PSObject.Properties.Value } Register-PSFTeppScriptblock -Name 'PSModuleDevelopment.Repository' -ScriptBlock { (Get-PSRepository).Name } Register-PSFTeppArgumentCompleter -Command Publish-PSMDStagedModule -Parameter Repository -Name 'PSModuleDevelopment.Repository' Register-PSFTeppArgumentCompleter -Command Set-PSMDStagingRepository -Parameter Repository -Name 'PSModuleDevelopment.Repository' Register-PSFTeppScriptblock -Name PSMD_templatestore -ScriptBlock { Get-PSFConfig -FullName "PSModuleDevelopment.Template.Store.*" | ForEach-Object { $_.Name -replace "^.+\." } } Register-PSFTeppScriptblock -Name PSMD_templatename -ScriptBlock { if ($fakeBoundParameter.Store) { $storeName = $fakeBoundParameter.Store } else { $storeName = "*" } $storePaths = Get-PSFConfig -FullName "PSModuleDevelopment.Template.Store.$storeName" | Select-Object -ExpandProperty Value $names = @() foreach ($path in $storePaths) { Get-ChildItem $path | Where-Object { $_.Name -match '-Info.xml$' } | ForEach-Object { $names += $_.Name -replace '-\d+(\.\d+){0,3}-Info.xml$' } } $names | Select-Object -Unique } #region Templates # New-PSMDDotNetProject Register-PSFTeppArgumentCompleter -Name PSMD_dotNetTemplates -Command New-PSMDDotNetProject -Parameter TemplateName Register-PSFTeppArgumentCompleter -Name PSMD_dotNetTemplatesUninstall -Command New-PSMDDotNetProject -Parameter Uninstall Register-PSFTeppArgumentCompleter -Name PSMD_dotNetTemplatesInstall -Command New-PSMDDotNetProject -Parameter Install # New-PSMDTemplate Register-PSFTeppArgumentCompleter -Name PSMD_templatestore -Command New-PSMDTemplate -Parameter OutStore # Get-PSMDTemplate Register-PSFTeppArgumentCompleter -Name PSMD_templatestore -Command Get-PSMDTemplate -Parameter Store Register-PSFTeppArgumentCompleter -Name PSMD_templatename -Command Get-PSMDTemplate -Parameter TemplateName # Invoke-PSMDTemplate Register-PSFTeppArgumentCompleter -Name PSMD_templatestore -Command Invoke-PSMDTemplate -Parameter Store Register-PSFTeppArgumentCompleter -Name PSMD_templatename -Command Invoke-PSMDTemplate -Parameter TemplateName Register-PSFTeppArgumentCompleter -Name psframework-encoding -Command Invoke-PSMDTemplate -Parameter Encoding # Remove-PSMDTemplate Register-PSFTeppArgumentCompleter -Name PSMD_templatestore -Command Remove-PSMDTemplate -Parameter Store Register-PSFTeppArgumentCompleter -Name PSMD_templatename -Command Remove-PSMDTemplate -Parameter TemplateName #endregion Templates #region Refactor Register-PSFTeppArgumentCompleter -Name psframework-encoding -Command Set-PSMDEncoding -Parameter Encoding #endregion Refactor $scriptBlock = { $webclient = New-Object System.Net.WebClient $string = $webclient.DownloadString("http://dotnetnew.azurewebsites.net/") $templates = $string -split "`n" | Select-String '<a href="/template/(.*?)/.*?">.*?</a>' | ForEach-Object { $_.Matches.Groups[1].Value } | Select-Object -Unique | Sort-Object Set-PSFTaskEngineCache -Module PSModuleDevelopment -Name "dotNetTemplates" -Value $templates } Register-PSFTaskEngineTask -Name "psmd_dotNetTemplateCache" -ScriptBlock $scriptBlock -Priority Low -Once -Description "Builds up the cache of installable templates for dotnet" $action = { param ( $Parameters ) $rootPath = $Parameters.RootPath $actualParameters = $Parameters.Parameters #region Process Parameters if (-not $actualParameters.Command) { throw "Mandatory parameter: Command not specified" } if ($actualParameters.Command -is [System.Management.Automation.ScriptBlock]) { $scriptblock = $actualParameters.Command } else { try { $scriptblock = [scriptblock]::Create($actualParameters.Command) } catch { throw "Error parsing command '$($actualParameters.Command)' : $_" } } $actualArguments = foreach ($argument in $actualParameters.ArgumentList) { if ($argument -isnot [string]) { $argument continue } if ($argument -notlike '%!*!%') { $argument continue } $artifactName = $argument -replace '^%!(.+)!%$', '$1' $artifactObject = Get-PSMDBuildArtifact -Name $artifactName if (-not $artifactObject) { throw "Artifact for arguments not found: $artifactName" } $artifactObject.Value } $inSession = $null if ($actualParameters.InSession) { $inSession = foreach ($sessionInput in $actualParameters.InSession) { if ($sessionInput -is [System.Management.Automation.Runspaces.PSSession]) { $sessionInput continue } $artifactObject = Get-PSMDBuildArtifact -Name $sessionInput if ($artifactObject.Value -is [System.Management.Automation.Runspaces.PSSession]) { $artifactObject.Value continue } if (-not $artifactObject) { throw "Artifact for parameter InSession not found: $($sessionInput)" } throw "Artifact for parameter InSession ($($sessionInput)) is not a pssession!" } } #endregion Process Parameters #region Execution $invokeParam = @{ ScriptBlock = $scriptblock ArgumentList = $actualArguments } if ($inSession) { $invokeParam.Session = $inSession } try { Invoke-Command @invokeParam -ErrorAction Stop } catch { throw } #endregion Execution } $params = @{ Name = 'command' Action = $action Description = 'Execute a scriptblock' Parameters = @{ Command = '(mandatory) Scriptcode to run' ArgumentList = 'Any number of arguments to pass to the command. To insert artifacts, specify a string with the special notation "%!ArtifactName!%"' InSession = 'Execute the scriptfile in the target PSSession. Either provide a full session object or an artifact name pointing at one.' } } Register-PSMDBuildAction @params $action = { param ( $Parameters ) $rootPath = $Parameters.RootPath $actualParameters = $Parameters.Parameters #region Utility Functions function ConvertTo-PSSession { [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true)] $InputObject ) process { if ($InputObject -is [System.Management.Automation.Runspaces.PSSession]) { return $InputObject } $artifactValue = (Get-PSMDBuildArtifact -Name $InputObject).Value if ($artifactValue -is [System.Management.Automation.Runspaces.PSSession]) { return $artifactValue } } } #endregion Utility Functions if (-not ($actualParameters.Path -and $actualParameters.Destination)) { throw "Invalid parameters! Specify both Path and Destination." } $paths = $actualParameters.Path -replace '%ProjectRoot%', $rootPath $copyParam = @{ Destination = $actualParameters.Destination -replace '%ProjectRoot%', $rootPath } if ($actualParameters.Recurse) { $copyParam.Recurse = $true } if ($actualParameters.Force) { $copyParam.Force = $true } if ($actualParameters.FromSession) { $fromSession = $actualParameters.FromSession | ConvertTo-PSSession if (-not $fromSession) { throw "FromSession $($actualParameters.FromSession) not found!" } $copyParam.FromSession = $fromSession } if ($actualParameters.ToSession) { $toSession = $actualParameters.ToSession | ConvertTo-PSSession if (-not $toSession) { throw "ToSession $($actualParameters.ToSession) not found!" } $copyParam.ToSession = $toSession } foreach ($path in $paths) { try { Copy-Item @copyParam -Path $path -ErrorAction Stop } catch { throw } } } $params = @{ Name = 'copy-item' Action = $action Description = 'Copies files & folders from A to B' Parameters = @{ Path = '(mandatory) Path(s) to copy. Use "%ProjectRoot%" to reference to the root path containing the build file.' Destination = '(mandatory) Path to copy to. Use "%ProjectRoot%" to reference to the root path containing the build file.' FromSession = 'Artifact Name of the PSSession to copy from.' ToSession = 'Artifact Name of the PSSession to copy to.' Recurse = 'Whether to copy child items' Force = 'Whether to use force (Remove destination items)' } } Register-PSMDBuildAction @params $action = { param ( $Parameters ) trap { if ($workingDirectory) { Remove-Item -Path $workingDirectory -Recurse -Force -ErrorAction SilentlyContinue } throw $_ } $rootPath = $Parameters.RootPath $actualParameters = $Parameters.Parameters #region Validate Input if (-not $actualParameters.Session) { throw "No Sessions specified!" } if ($actualParameters.Session | Where-Object State -NE Opened) { throw "Sessions not open!" } if ($actualParameters.Repository -and (-not (Get-PSRepository -Name $actualParameters.Repository -ErrorAction Ignore))) { throw "Repository $($actualParameters.Repository) not found!" } foreach ($module in $actualParameters.Module) { if ($module -notmatch '\\|/') { continue } try { $null = Resolve-PSFPath -Path $module -Provider FileSystem } catch { throw "Unable to resolve path: $module"} } #endregion Validate Input #region Prepare modules to transfer $workingDirectory = Join-Path -Path (Get-PSFPath -Name temp) -ChildPath "psmd_action_$(Get-Random)" $null = New-Item -Path $workingDirectory -ItemType Directory -Force -ErrorAction Stop $saveModuleParam = @{ Path = $workingDirectory Repository = $actualParameters.Repository } foreach ($module in $actualParameters.Module) { if ($module -notmatch '\\|/') { if ($actualParameters.Repository) { Save-Module $module @saveModuleParam continue } $moduleObject = Get-Module -Name $module -ListAvailable | Sort-Object Version -Descending | Select-Object -First 1 if (-not $moduleObject) { throw "Cannot find module $module!" } Copy-Item -Path $moduleObject.ModuleBase -Destination "$workingDirectory\$($moduleObject.Name)" -Recurse -Force continue } foreach ($path in Resolve-PSFPath -Path $module -Provider FileSystem) { if (Test-Path -LiteralPath $path -PathType Leaf) { $path = Split-Path -LiteralPath $path } Copy-Item -LiteralPath $path -Destination $workingDirectory -Recurse -Force } } #endregion Prepare modules to transfer foreach ($moduleFolder in Get-ChildItem -Path $workingDirectory) { if (-not $actualParameters.NoDelete) { Invoke-Command -Session $actualParameters.Session -ScriptBlock { param ($Name) if (-not (Test-Path -Path "$env:ProgramFiles\WindowsPowerShell\Modules\$Name")) { return } Remove-Item -Path "$env:ProgramFiles\WindowsPowerShell\Modules\$Name" -Recurse -Force } -ArgumentList $moduleFolder.Name } foreach ($session in $actualParameters.Session) { Copy-Item -LiteralPath $moduleFolder.FullName -Destination "$env:ProgramFiles\WindowsPowerShell\Modules" -Recurse -Force -ToSession $session -ErrorAction Stop } } } $params = @{ Name = 'deployModule' Action = $action Description = 'Deploys a module to the target computer(s)' Parameters = @{ Session = '(mandatory) The PSRemoting sessions to deploy the module through.' Module = '(mandatory) A list of names or paths of modules to deploy. Can be used in any combination, specifying by name will use the latest version found on the local computer unless also using he "Repository" parameter to specify an alternate source.' Repository = 'The repository from which to download the module(s) (and any dependencies). Modules will be sourced locally if empty.' NoDelete = '[bool] Whether to keep other versions of the target module on the remote machine. By default, all other versions will be deleted.' } } Register-PSMDBuildAction @params $action = { param ( $Parameters ) $rootPath = $Parameters.RootPath $actualParameters = $Parameters.Parameters if (-not $actualParameters.ArtifactName) { throw "No ArtifactName specified! Unable to publish remoting session for build." } if (-not ($actualParameters.VMName -or $actualParameters.ComputerName)) { throw "Neither ComputerName nor VMName specified, unable to connect to nothing!" } if ($actualParameters.VMName -and $actualParameters.ComputerName) { throw "Both ComputerName and VMName specified, unable to connect to both at once!" } $credential = $null if ($actualParameters.CredentialPath) { $path = $actualParameters.CredentialPath -replace '%ProjectRoot%', $rootPath try { $credential = Import-PSFClixml -Path $path -ErrorAction Stop } catch { throw "Error accessing credentials from $path : $_" } } if ($actualParameters.Credential) { if ($actualParameters.Credential -isnot [pscredential]) { throw "Not a credential object: $($actualParameters.Credential)" } $credential = $actualParameters.Credential } $paramNewPSSession = @{ } if ($actualParameters.VMName) { $paramNewPSSession.VMName = $actualParameters.VMName } if ($actualParameters.ComputerName) { $paramNewPSSession.ComputerName = $actualParameters.ComputerName } if ($actualParameters.Port) { $paramNewPSSession.Port = $actualParameters.Port } if ($credential) { $paramNewPSSession.Credential = $credential } try { $session = New-PSSession @paramNewPSSession -ErrorAction Stop } catch { throw "Error establishing PS Remoting session: $_" } Publish-PSMDBuildArtifact -Name $actualParameters.ArtifactName -Value $session -Tag pssession } $params = @{ Name = 'new-pssession' Action = $action Description = 'Establish a PSSession to a target computer and provide it as an artifact' Parameters = @{ ComputerName = 'The Computer to connect to' Port = 'Port you want to connect to' VMName = 'The virtual machine to which to connect to via the HyperV VM Bus' CredentialPath = 'The path to the credentials to use for the connection. Use %ProjectRoot% to insert the folder path to where the buildfile is located' Credential = 'PSCredential object to use for authenticatioon' ArtifactName = '(mandatory) The name under which to publish the session as an artifact' } } Register-PSMDBuildAction @params $action = { param ( $Parameters ) $rootPath = $Parameters.RootPath $actualParameters = $Parameters.Parameters if (-not $actualParameters.Path) { throw "Invalid parameters! Specify a Path to delete." } $paths = $actualParameters.Path -replace '%ProjectRoot%', $rootPath $deleteParam = @{ } if ($actualParameters.Recurse) { $deleteParam.Recurse = $true } if ($actualParameters.Force) { $deleteParam.Force = $true } $inSession = $null if ($actualParameters.InSession) { if ($actualParameters.InSession -is [System.Management.Automation.Runspaces.PSSession]) { $inSession = $actualParameters.InSession } $artifactObject = Get-PSMDBuildArtifact -Name $actualParameters.InSession if (-not $artifactObject) { throw "Artifact for parameter InSession not found: $($actualParameters.InSession)" } if ($artifactObject.Value -isnot [System.Management.Automation.Runspaces.PSSession]) { throw "Artifact for parameter InSession ($($actualParameters.InSession)) is not a pssession!" } $inSession = $artifactObject.Value } if ($inSession) { $failed = Invoke-Command -Session $inSession -ScriptBlock { param ($DeleteParam, $Paths) foreach ($path in $Paths) { if (-not (Get-Item -Path $path -Force -ErrorAction Ignore)) { continue } try { Remove-Item @DeleteParam -Path $path -ErrorAction Stop } catch { return $_ } } } -ArgumentList $deleteParam, $paths if ($failed) { throw $failed } } foreach ($path in $paths) { if (-not (Get-Item -Path $path -Force -ErrorAction Ignore)) { continue } try { Remove-Item @DeleteParam -Path $path -ErrorAction Stop } catch { throw } } } $params = @{ Name = 'remove-item' Action = $action Description = 'Removes files or folders' Parameters = @{ Path = '(mandatory) Path(s) to the item(s) to delete. Use "%ProjectRoot%" to reference to the root path containing the build file.' InSession = 'Artifact Name of the PSSession within which to execute the deletion' Recurse = 'Whether to delete child items' Force = 'Whether to use force' } } Register-PSMDBuildAction @params $action = { param ( $Parameters ) $rootPath = $Parameters.RootPath $actualParameters = $Parameters.Parameters if ($actualParameters.All) { foreach ($artifact in Get-PSMDBuildArtifact -Tag pssession) { try { $artifact.Value | Remove-PSSession -ErrorAction Stop Remove-PSMDBuildArtifact -Name $artifact.Name } catch { throw "Failed to remove PSSession artifact $($artifact.Name) to $($artifact.Value) | $_" } } } elseif ($actualParameters.ArtifactName) { $artifact = Get-PSMDBuildArtifact -Name $actualParameters.ArtifactName if ($artifact) { try { $artifact.Value | Remove-PSSession -ErrorAction Stop Remove-PSMDBuildArtifact -Name $artifact.Name } catch { throw "Failed to remove PSSession artifact $($artifact.Name) to $($artifact.Value) | $_" } } } else { throw "Invalid parameters! Specify either 'All' or 'ArtifactName' in step definition." } } $params = @{ Name = 'remove-pssession' Action = $action Description = 'Removes a PSSession that was previously established with the new-pssession action' Parameters = @{ ArtifactName = 'The name under which to publish the session as an artifact' All = 'Whether all PSSession artifacts should be removed' } } Register-PSMDBuildAction @params $action = { param ( $Parameters ) $rootPath = $Parameters.RootPath $actualParameters = $Parameters.Parameters #region Process Parameters if (-not $actualParameters.Path) { throw "Mandatory parameter: Path not specified" } $scriptPath = $actualParameters.Path -replace '%ProjectRoot%', $rootPath if (-not (Test-Path $scriptPath)) { throw "Cannot find resolved script path: $scriptPath" } $actualArguments = foreach ($argument in $actualParameters.ArgumentList) { if ($argument -isnot [string]) { $argument continue } if ($argument -notlike '%!*!%') { $argument continue } $artifactName = $argument -replace '^%!(.+)!%$', '$1' $artifactObject = Get-PSMDBuildArtifact -Name $artifactName if (-not $artifactObject) { throw "Artifact for arguments not found: $artifactName" } $artifactObject.Value } $inSession = $null if ($actualParameters.InSession) { if ($actualParameters.InSession -is [System.Management.Automation.Runspaces.PSSession]) { $inSession = $actualParameters.InSession } $artifactObject = Get-PSMDBuildArtifact -Name $actualParameters.InSession if (-not $artifactObject) { throw "Artifact for parameter InSession not found: $($actualParameters.InSession)" } if ($artifactObject.Value -isnot [System.Management.Automation.Runspaces.PSSession]) { throw "Artifact for parameter InSession ($($actualParameters.InSession)) is not a pssession!" } $inSession = $artifactObject.Value } #endregion Process Parameters #region Execution $invokeParam = @{ FilePath = $scriptPath ArgumentList = $actualArguments } if ($inSession) { $invokeParam.Session = $inSession } try { Invoke-Command @invokeParam -ErrorAction Stop } catch { throw } #endregion Execution } $params = @{ Name = 'script' Action = $action Description = 'Execute a scriptfile' Parameters = @{ Path = '(mandatory) Path to the scriptfile to run. Use %ProjectRoot% to reference the same folder the build action file is stored in.' ArgumentList = 'Any number of arguments to pass to the scripts. To insert artifacts, specify a string with the special notation "%!ArtifactName!%"' InSession = 'Execute the scriptfile in the target PSSession. Either provide a full session object or an artifact name pointing at one.' } } Register-PSMDBuildAction @params New-PSFLicense -Product 'PSModuleDevelopment' -Manufacturer 'Friedrich Weinmann' -ProductVersion $script:ModuleVersion -ProductType Module -Name MIT -Version "1.0.0.0" -Date (Get-Date "2017-04-27") -Text @" Copyright (c) 2017 Friedrich Weinmann Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. "@ $__modules = Get-PSMDModuleDebug | Sort-Object Priority foreach ($__module in $__modules) { if ($__module.AutoImport) { try { . Import-PSMDModuleDebug -Name $__module.Name -ErrorAction Stop } catch { Write-PSFMessage -Level Warning -Message "Failed to import Module: $($__module.Name)" -Tag import -ErrorRecord $_ -Target $__module.Name } } } #endregion Load compiled code |