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" } } Set-PSFConfig -Module PSModuleDevelopment -Name 'Debug.ConfigPath' -Value "$($path_FileUserShared)\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.' # 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') -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 "$path_FileUserShared\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 "$env: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 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 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 #> [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 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 .NOTES Version 1.0.0.0 Author: Friedrich Weinmann Created on: August 15th, 2016 #> [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 } } } New-Alias -Name hex -Value Get-PSMDHelp -Scope Global -Option AllScope 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. #> [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() } } } New-Alias -Name ipmod -Value Import-ModuleDebug -Option AllScope -Scope Global 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) #> [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 } } Set-Alias -Name smd -Value Set-PSMDModuleDebug -Option AllScope -Scope Global 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 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 $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$') { return $false } if (-not ($args[0].CommandElements.ParameterName -match '^String$|^ActionString$')) { return $false } $true }, $true) foreach ($commandAst in $commandAsts) { $stringParam = $commandAst.CommandElements | Where-Object ParameterName -match '^String$|^ActionString$' $stringParamValue = $commandAst.CommandElements[($commandAst.CommandElements.IndexOf($stringParam) + 1)].Value $stringValueParam = $commandAst.CommandElements | Where-Object ParameterName -match '^StringValues$|^ActionStringValues$' 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 } } # Additional checks for splatted commands # find all splatted commands $splattedVariables = $ast.FindAll( { if ($args[0] -isnot [System.Management.Automation.Language.VariableExpressionAst ]) { return $false } if (-not ($args[0].Splatted -eq $true)) { return $false } $true }, $true) foreach ($splattedVariable in $splattedVariables) { #get the variable name $splatParamName = $splattedVariable.VariablePath.UserPath if ($splatParamName) { # match the $param = @{ $splatParamNameRegex = "^\s?\`$$($splatParamName)\s?=\s?\@\{" # get all variable assignments where the # left side matches our param # operator is = # matches our assignment regex $splatAssignmentAsts = $ast.FindAll( { if ($args[0] -isnot [System.Management.Automation.Language.AssignmentStatementAst ]) { return $false } if (-not ($args[0].Left -match $splatParamName)) { return $false } if (-not ($args[0].Operator -eq 'Equals')) { return $false } if (-not ($args[0].Extent -match $splatParamNameRegex)) { return $false } $true }, $true) foreach ($splatAssignmentAst in $splatAssignmentAsts) { # get the hashtable $splatHashTable = $splatAssignmentAst.Right.Expression # see if its an empty assignment or null if ($splatHashTable -and $splatHashTable.KeyValuePairs.Count -gt 0) { # find any String or ActionString $splatParam = $splatAssignmentAst.Right.Expression.KeyValuePairs | Where-Object Item1 -match '^String$|^ActionString$' # The kvp.item.extent.text returns nested quotes where as the commandast.extent.text doesn't so strip them off $splatParamValue = $splatParam.Item2.Extent.Text.Trim('"').Trim("'") # find any StringValue or ActionStringValue $splatValueParam = $splatAssignmentAst.Right.Expression.KeyValuePairs | Where-Object Item1 -match '^StringValues$|^ActionStringValues$' if ($splatValueParam) { # The kvp.item.extent.text returns nested quotes whereas the commandast.extent.text doesn't so strip them off $splatValueParamValue = $splatValueParam.Item2.Extent.Text.Trim('"').Trim("'") } else { $splatValueParamValue = '' } [PSCustomObject]@{ PSTypeName = 'PSModuleDevelopment.String.ParsedItem' File = $file.FullName Line = $splatHashTable.Extent.StartLineNumber CommandName = $splattedVariable.Parent.CommandElements[0].Value String = $splatParamValue StringValues = $splatValueParamValue } } } } } $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 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 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 #> [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 } } } } Set-Alias -Name parse -Value Read-PSMDScript 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.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", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [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 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 { Write-PSFMessage -Level InternalComment -Message "Bound parameters: $($PSBoundParameters.Keys -join ", ")" -Tag 'debug', 'start', 'param' $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-Clixml $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-Clixml $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 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 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" #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSPossibleIncorrectUsageOfAssignmentOperator", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')] [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)] [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] $Force, [switch] $Silent, [switch] $EnableException ) begin { #region Validate output path try { $resolvedPath = Resolve-Path $OutPath -ErrorAction Stop if (($resolvedPath | Measure-Object).Count -ne 1) { throw "Cannot resolve $OutPath to a single folder" } if ($resolvedPath.Provider -notlike "*FileSystem") { throw "Path $OutPath was not recognized as a filesystem path" } } catch { Stop-PSFFunction -Message "Could not resolve output path to a valid folder: $OutPath" -EnableException $EnableException -ErrorRecord $_ -Tag 'fail', 'path', 'validate' return } #endregion Validate output path $templates = @() switch ($PSCmdlet.ParameterSetName) { 'NameStore' { $templates = Get-PSMDTemplate -TemplateName $TemplateName -Store $Store } 'NamePath' { $templates = Get-PSMDTemplate -TemplateName $TemplateName -Path $Path } } #region Parameter Processing if (-not $Parameters) { $Parameters = @{ } } if ($Name) { $Parameters["Name"] = $Name } foreach ($config in (Get-PSFConfig -Module 'PSModuleDevelopment' -Name 'Template.ParameterDefault.*')) { $cfgName = $config.Name -replace '^.+\.([^\.]+)$', '$1' if (-not $Parameters.ContainsKey($cfgName)) { $Parameters[$cfgName] = $config.Value } } #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, [bool] $Silent ) Write-PSFMessage -Level Verbose -Message "Processing template $($item)" -Tag 'template', 'invoke' -FunctionName Invoke-PSMDTemplate $templateData = Import-Clixml -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 switch ($templateData.Type.ToString()) { #region File "File" { foreach ($child in $templateData.Children) { Write-TemplateItem -Item $child -Path $OutPath -Encoding $Encoding -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) { Write-TemplateItem -Item $child -Path $newFolder.FullName -Encoding $Encoding -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)þþþ","" } $configFile = Join-Path $newFolder.FullName "PSMDTemplate.ps1" Set-Content -Path $configFile -Value $optionsTemplate -Encoding ([PSFEncoding]'utf-8').Encoding } #endregion Write Config File (Raw) } #endregion Project } } function Write-TemplateItem { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [PSModuleDevelopment.Template.TemplateItemBase] $Item, [string] $Path, [PSFEncoding] $Encoding, [hashtable] $ParameterFlat, [hashtable] $ParameterScript, [bool] $Raw ) Write-PSFMessage -Level Verbose -Message "Creating file: $($Item.Name) ($($Item.RelativePath))" -FunctionName Invoke-PSMDTemplate -ModuleName PSModuleDevelopment -Tag 'create','template' $identifier = $Item.Identifier $isFile = $Item.GetType().Name -eq 'TemplateItemFile' #region File if ($isFile) { $fileName = $Item.Name if (-not $Raw) { foreach ($param in $Item.FileSystemParameterFlat) { $fileName = $fileName -replace "$($identifier)$([regex]::Escape($param))$($identifier)",$ParameterFlat[$param] } foreach ($param in $Item.FileSystemParameterScript) { $fileName = $fileName -replace "$($identifier)!$([regex]::Escape($param))!$($identifier)", $ParameterScript[$param] } } $destPath = Join-Path $Path $fileName if ($Item.PlainText) { $text = $Item.Value if (-not $Raw) { foreach ($param in $Item.ContentParameterFlat) { $text = $text -replace "$($identifier)$([regex]::Escape($param))$($identifier)", $ParameterFlat[$param] } foreach ($param in $Item.ContentParameterScript) { $text = $text -replace "$($identifier)!$([regex]::Escape($param))!$($identifier)", $ParameterScript[$param] } } [System.IO.File]::WriteAllText($destPath, $text, $Encoding) } else { $bytes = [System.Convert]::FromBase64String($Item.Value) [System.IO.File]::WriteAllBytes($destPath, $bytes) } } #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 = New-Item -Path $Path -Name $folderName -ItemType Directory foreach ($child in $Item.Children) { Write-TemplateItem -Item $child -Path $folder.FullName -Encoding $Encoding -ParameterFlat $ParameterFlat -ParameterScript $ParameterScript -Raw $Raw } } #endregion Folder } #endregion Helper function } process { if (Test-PSFFunctionInterrupt) { return } foreach ($item in $Template) { if ($PSCmdlet.ShouldProcess($item, "Invoking template")) { try { Invoke-Template -Template $item -OutPath $resolvedPath.ProviderPath -NoFolder $NoFolder -Encoding $Encoding -Parameters $Parameters.Clone() -Raw $Raw -Silent $Silent } catch { Stop-PSFFunction -Message "Failed to invoke template $($item)" -EnableException $EnableException -ErrorRecord $_ -Target $item -Tag 'fail', 'template', 'invoke' -Continue } } } foreach ($item in $templates) { if ($PSCmdlet.ShouldProcess($item, "Invoking template")) { try { Invoke-Template -Template $item -OutPath $resolvedPath.ProviderPath -NoFolder $NoFolder -Encoding $Encoding -Parameters $Parameters.Clone() -Raw $Raw -Silent $Silent } catch { Stop-PSFFunction -Message "Failed to invoke template $($item)" -EnableException $EnableException -ErrorRecord $_ -Target $item -Tag 'fail', 'template', 'invoke' -Continue } } } } } if (-not (Test-Path Alias:\imt)) { Set-Alias -Name imt -Value Invoke-PSMDTemplate } 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', '')] [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 } } } New-Alias -Name dotnetnew -Value New-PSMDDotNetProject -Option AllScope -Scope Global -ErrorAction Ignore 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-Clixml -Path (Join-Path $exportFolder $fileName) $template.ToTemplateInfo() | Export-Clixml -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'. #> [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 } } New-Alias -Name find -Value Find-PSMDFileContent -Scope Global -Option AllScope 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. .NOTES Version 1.0.0.0 Author: Friedrich Weinmann Created on: August 6th, 2016 #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low')] Param ( [Switch] $NoExit, [Switch] $Admin, [switch] $NoProfile ) begin { $powershellPath = (Get-Process -id $pid).Path } process { if ($PSCmdlet.ShouldProcess("Current shell", "Restart")) { if ($NoProfile) { if ($Admin) { Start-Process $powershellPath -Verb RunAs -ArgumentList '-NoProfile' } else { Start-Process $powershellPath -ArgumentList '-NoProfile' } } else { if ($Admin) { Start-Process $powershellPath -Verb RunAs } else { Start-Process $powershellPath } } if (-not $NoExit) { exit } } } } New-Alias -Name Restart-Shell -Value Restart-PSMDShell -Option AllScope -Scope Global New-Alias -Name rss -Value Restart-PSMDShell -Option AllScope -Scope Global 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())" } } # Dummy file to make architecture happy 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 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" 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 |