functions/public.ps1
#region define an object class for the Get-PSFunctionInfo commmand class PSFunctionInfo { [string]$Name [version]$Version [string]$Description [string]$Author [string]$Source [string]$Module [string]$CompanyName [string]$Copyright [guid]$Guid [string[]]$Tags [datetime]$LastUpdate [string]$Commandtype #this class has no methods #constructors PSFunctionInfo([string]$Name, [string]$Source) { $this.Name = $Name $this.Source = $Source } PSFunctionInfo([string]$Name, [string]$Author, [string]$Version, [string]$Source, [string]$Description, [string]$Module, [string]$CompanyName, [string]$Copyright, [guid]$Guid, [datetime]$LastUpdate, [string]$Commandtype) { $this.Name = $Name $this.Author = $Author $this.Version = $Version $this.Source = $Source $this.Description = $Description $this.Module = $Module $this.CompanyName = $CompanyName $this.Copyright = $Copyright #$this.Tags = $Tags $this.guid = $Guid $this.LastUpdate = $LastUpdate $this.CommandType = $Commandtype } } #endregion Function New-PSFunctionInfo { [cmdletbinding(SupportsShouldProcess)] [alias('npfi')] Param( [Parameter(Position = 0, Mandatory, HelpMessage = "Specify the name of the function")] [ValidateNotNullOrEmpty()] [string]$Name, [Parameter(Mandatory, HelpMessage = "Specify the path that contains the function")] [ValidateNotNullOrEmpty()] [ValidateScript( { Test-Path $_ })] [ValidatePattern("\.ps1$")] [string]$Path, [string]$Author = [System.Environment]::UserName, [string]$CompanyName, [string]$Copyright = (Get-Date).Year, [string]$Description, [ValidateNotNullOrEmpty()] [string]$Version = "1.0.0", [string[]]$Tags, [Parameter(HelpMessage = "Copy the metadata to the clipboard. The file is left untouched.")] [alias("clip")] [switch]$ToClipboard ) Begin { Write-Verbose "[$((Get-Date).TimeofDay) BEGIN ] Starting $($myinvocation.mycommand)" [guid]$Guid = $(([guid]::NewGuid()).guid) [string]$updated = Get-Date -Format g } #begin Process { Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Creating this metadata" $info = @" <# PSFunctionInfo Version $Version Author $Author CompanyName $CompanyName Copyright $Copyright Description $Description Guid $Guid Tags $($Tags -join ",") LastUpdate $Updated Source $(Convert-Path $Path) #> "@ Write-Verbose $info if ($ToClipboard) { Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Copying the metadata to clipboard" if ($pscmdlet.shouldprocess("function metadata", "Copy to clipboard")) { Set-Clipboard -Value $info } } else { #get the contents of the script file Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Getting the file contents from $Path" $file = [System.Collections.Generic.list[string]]::New() Get-Content -Path $path | ForEach-Object { $file.add($_) } #find the function line Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Searching for Function $Name" $find = $file | Select-String -Pattern "^(\s+)?(F|f)unction $name(\s+|\{)?" if ($find.count -gt 1) { Write-Warning "Detected multiple matches for Function $name in $Path. Unable to insert metadata." #bail out return } elseif ($find.count -eq 1) { #$index = $file.findIndex( { $args[0] -match "^(\s+)?Function $name(\s+|\{)" }) #the index for the file list will be 1 less than the pattern match $index = $find.Linenumber - 1 } else { Write-Warning "Failed to find a function called $Name in $path." return } #find the opening { for the function Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Getting the position of the opening {" $i = $index do { Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Testing index $i" if ($file[$i] -match "\{") { Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Found opening { at $i" $found = $True } else { Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] incrementing index" $i++ } } until ($found -OR $i -gt $file.count) if ($i -gt $file.count) { Write-Warning "Failed to find the opening { for Function $Name." return } #test for an existing PSFunctionInfo entry in the next 5 lines if ($file[$i..($i + 5)] | Select-String -Pattern "PSFunctionInfo" -Quiet) { Write-Warning "An existing PSFunctionInfo entry has been detected." } else { # insert after the opening { Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Inserting metadata at position $($i+1)" $file.Insert(($i + 1), $info) #write the new data to the file Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Updating $path" $file | Set-Content -Path $Path } } #else process the file } #process End { Write-Verbose "[$((Get-Date).TimeofDay) END ] Ending $($myinvocation.mycommand)" } #end } #close New-PSFunctionInfo #get function info from non-module functions Function Get-PSFunctionInfo { [cmdletbinding(DefaultParameterSetName = "name")] [outputtype("PSFunctionInfo")] [alias("gpfi")] Param( [Parameter( Position = 0, HelpMessage = "Specify the name of a function that doesn't belong to a module.", ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = "name" )] [string]$FunctionName = "*", [Parameter( HelpMessage = "Specify a .ps1 file to search.", ValueFromPipelineByPropertyName, ParameterSetName = "file" )] [ValidatePattern('\.ps1$')] [ValidateScript( { Test-Path $_ })] [alias("fullname")] [string]$Path, [Parameter(HelpMessage = "Specify a tag")] [string]$Tag ) Begin { Write-Verbose "[$((Get-Date).TimeofDay) BEGIN ] Starting $($myinvocation.mycommand)" #a regex pattern that will be used to parse the metadata from the function definition [regex]$rx = "(?<property>\w+)\s+(?<value>.*)" } #begin Process { Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Using parameter set $($pscmdlet.ParameterSetName)" if ($pscmdlet.ParameterSetName -eq 'file') { $file = [System.Collections.Generic.list[string]]::New() Get-Content -Path $path | ForEach-Object { $file.add($_) } #get location of PSFunctionInfo $start = 0 #[regex]$rxName = "(\s+)?Function\s+\S+" #need to ignore case $rxname =[System.Text.RegularExpressions.Regex]::new("(\s+)?[Ff]unction\s+\S+","IgnoreCase") do { Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Searching $path at $start" $i = $j = $file.FindIndex( $start, { $args[0] -match "#(\s+)?PSFunctionInfo" }) if ($i -gt 0) { Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Metadata found at index $i" do { $funName = $rxName.Match($file[$i]).value.trim() $i-- } until ($i -lt 0 -OR $funName) $name = $funName.split()[1] Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Found function $Name" $h = @{Name = $name } do { $j++ #trim off spaces in case the comment block is indented $line = $file[$j].trim() if ($line -match "\w+\s+\w+") { $meta = $line.split(" ", 2) $h.add($meta[0], $meta[1]) } } Until ($j -gt $file.count -OR $line -match "#>") #$h | out-string | write-verbose Try { $out = new_psfunctioninfo @h -ErrorAction Stop $out.CommandType = "function" Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Filtering for tag $tag" if ($PSBoundParameters.ContainsKey("Tag") -AND ($out.Tags -match $Tag)) { $out } elseif (-Not $PSBoundParameters.ContainsKey("Tag")) { $out } } Catch { Write-Warning "Failed to process $Path. $($_.Exception.message)." } $start = $j } } Until ($i -lt 0) } else { Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Getting function $FunctionName" # filter out functions with a module source and that pass the private filtering test $functions = (Get-ChildItem -Path Function:\$FunctionName).where( { -Not $_.source -And (test_functionname $_.name) }) Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Found $($functions.count) functions" Foreach ($fun in $functions) { Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] $($fun.name)" $definition = $fun.definition -split "`n" $m = $definition | Select-String -Pattern "#(\s+)?PSFunctionInfo" if ($m.count -gt 1) { Write-Warning "Multiple matches found for PSFunctionInfo in $($fun.name). Will only process the first one." } if ($m) { #get the starting line number $i = $m[0].LineNumber $meta = While ($definition[$i] -notmatch "#\>") { $raw = $definition[$i] if ($raw -match "\w+") { $raw } $i++ } #Define a hashtable that will eventually become a custom object $h = @{ Name = $fun.name CommandType = $fun.CommandType Module = $fun.Module } #parse the metadata using regular expressions Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Parsing metadata" for ($i = 0; $i -lt $meta.count; $i++) { $groups = $rx.Match($meta[$i]).groups Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] $($groups[1].value) = $($groups[2].value)" $h.add($groups[1].value, $groups[2].value.trim()) } #check for required properties if (-Not ($h.ContainsKey("Source")) ) { $h.add("Source", "") } if (-Not ($h.ContainsKey("version"))) { $h.add("Version", "") } #$h | Out-String | Write-Verbose #write the custom object to the pipeline $fi = New-Object -TypeName PSFunctionInfo -ArgumentList $h.name, $h.version #update the object with hash table properties foreach ($key in $h.keys) { Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Updating $key [$($h.$key)]" $fi.$key = $h.$key } if ($tag) { Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Filtering for tag $tag" # write-verbose "$($fi.name) tag: $($fi.tags)" if ($fi.tags -match $tag) { $fi } } else { $fi } #clear the variable so it doesn't get reused Remove-Variable m, h } #if metadata found else { #insert the custom type name and write the object to the pipeline Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Creating a new and temporary PSFunctionInfo object." $fi = New-Object PSFunctionInfo -ArgumentList $fun.name, $fun.source $fi.version = $fun.version $fi.module = $fun.Module $fi.Commandtype = $fun.CommandType $fi.Description = $fun.Description #Write the object depending on the parameter set and if it belongs to a module AND has a source if (-Not $tag) { $fi } } } } #foreach } #process End { Write-Verbose "[$((Get-Date).TimeofDay) END ] Ending $($myinvocation.mycommand)" } #end } #close Get-PSFunctionInfo Function Get-PSFunctionInfoTag { [cmdletbinding()] [outputtype("String")] Param() Write-Verbose "[$((Get-Date).TimeofDay)] Starting $($myinvocation.mycommand)" $taglist = [System.Collections.Generic.list[string]]::new() Write-Verbose "[$((Get-Date).TimeofDay)] Getting unique tags from Get-PSFunctionInfo" $items = (Get-PSFunctionInfo -ErrorAction stop).tags | Select-Object -Unique if ($items.count -eq 0) { Write-Warning "Failed to find any matching functions with tags" } else { Write-Verbose "[$((Get-Date).TimeofDay)] Found at least $($items.count) tags" foreach ($item in $items) { if ($item -match ",") { #split strings into an array $item.split(",") | ForEach-Object { if (-Not $taglist.contains($_)) { $taglist.add($_.trim()) } } } #if an array of tags else { if (-Not $taglist.contains($item)) { $taglist.add($item.trim()) } } } #foreach item } #else #write the list to the pipeline $taglist | Sort-Object Write-Verbose "[$((Get-Date).TimeofDay)] Ending $($myinvocation.mycommand)" } <# Version $Version Author $Author CompanyName $CompanyName Copyright $Copyright Description $Description Guid $Guid Tags $($Tags -join ",") LastUpdate $Updated Source $(Convert-Path $Path) #> Function Set-PSFunctionInfoDefaults { [cmdletbinding(SupportsShouldProcess)] Param( [Parameter(ValueFromPipelineByPropertyName, HelpMessage = "Enter the default author name.")] [string]$Author, [Parameter(ValueFromPipelineByPropertyName, HelpMessage = "Enter the default company name.")] [string]$CompanyName, [Parameter(ValueFromPipelineByPropertyName, HelpMessage = "Enter the default copyright string")] [string]$Copyright, [Parameter(ValueFromPipelineByPropertyName, HelpMessage = "Enter the default version")] [string]$Version, [Parameter(ValueFromPipelineByPropertyName, HelpMessage = "Enter the default tag(s).")] [string[]]$Tags ) Begin { Write-Verbose "[$((Get-Date).TimeofDay) BEGIN ] Starting $($myinvocation.mycommand)" $Outfile = Join-Path $home -ChildPath psfunctioninfo-defaults.json #remove common and optional parameters if bound $common = [System.Management.Automation.Cmdlet]::CommonParameters $option = [System.Management.Automation.Cmdlet]::OptionalCommonParameters $option | ForEach-Object { #Write-Verbose "Testing for $_" if ($PSBoundParameters.ContainsKey($_)) { Write-Verbose "[$((Get-Date).TimeofDay) BEGIN ] Removing $_" [void]$PSBoundParameters.remove($_) } } $common | ForEach-Object { #Write-Verbose "Testing for $_" if ($PSBoundParameters.ContainsKey($_)) { Write-Verbose "[$((Get-Date).TimeofDay) BEGIN ]Removing $_" [void]$PSBoundParameters.remove($_) } } #get existing defaults if (Test-Path -Path $outfile) { Write-Verbose "[$((Get-Date).TimeofDay) BEGIN ] Getting current defaults" $current = Get-PSFunctionInfoDefaults } } #begin Process { Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Using these new defaults" $PSBoundParameters | Out-String | Write-Verbose if ($current) { Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Updating current defaults" $PSBoundParameters.GetEnumerator() | ForEach-Object { if ($current.$($_.key)) { Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] ...$($_.key)" $current.$($_.key) = $_.value } else { #add new values Add-Member -InputObject $current -MemberType NoteProperty -Name $_.key -Value $_.value -Force } } $defaults = $current } else { $defaults = $PSBoundParameters } Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Saving results to $Outfile" $defaults | Out-String | Write-Verbose $defaults | ConvertTo-Json | Out-File -FilePath $Outfile -Force } #process End { If (-Not $WhatIfPreference) { Write-Verbose "[$((Get-Date).TimeofDay) END ] Re-import the module or run Update-PSFunctionInfoDefaults to load the new values." } Write-Verbose "[$((Get-Date).TimeofDay) END ] Ending $($myinvocation.mycommand)" } #end } #close Set-PSFunctionInfoDefaults Function Get-PSFunctionInfoDefaults { [cmdletbinding()] [outputtype("PSFunctionInfoDefault")] Param( ) Begin { Write-Verbose "[$((Get-Date).TimeofDay) BEGIN ] Starting $($myinvocation.mycommand)" $Outfile = Join-Path $home -ChildPath psfunctioninfo-defaults.json } #begin Process { Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Testing $outfile" If (Test-Path -Path $outfile) { Get-Content -Path $outfile | ConvertFrom-Json | ForEach-Object { $_.psobject.typenames.insert(0, 'PSFunctionInfoDefault') $_ } } else { Write-Warning "No default file found at $outfile. Use Set-PSFunctionInfoDefaults to create it." } } #process End { Write-Verbose "[$((Get-Date).TimeofDay) END ] Ending $($myinvocation.mycommand)" } #end } #close Get-PSFunctionInfoDefaults Function Update-PSFunctionInfoDefaults { [cmdletbinding(SupportsShouldProcess)] Param( ) Begin { Write-Verbose "[$((Get-Date).TimeofDay) BEGIN ] Starting $($myinvocation.mycommand)" $defaults = Join-Path $home -ChildPath psfunctioninfo-defaults.json } #begin Process { Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Updating PSDefaultParameterValues " if (Test-Path -Path $defaults) { $d = Get-Content -Path $defaults | ConvertFrom-Json $d.psobject.properties | ForEach-Object { if ($pscmdlet.ShouldProcess($_.name)) { $global:PSDefaultParameterValues["New-PSFunctionInfo:$($_.name)"] = $_.value } } } } #process End { Write-Verbose "[$((Get-Date).TimeofDay) END ] Ending $($myinvocation.mycommand)" } #end } #close Update-PSFunctionInfoDefaults |