diff --git a/PSFramework/bin/PSFramework.dll b/PSFramework/bin/PSFramework.dll
index 5b657725..6f1712d4 100644
Binary files a/PSFramework/bin/PSFramework.dll and b/PSFramework/bin/PSFramework.dll differ
diff --git a/PSFramework/bin/PSFramework.pdb b/PSFramework/bin/PSFramework.pdb
index e942741f..7114f4fc 100644
Binary files a/PSFramework/bin/PSFramework.pdb and b/PSFramework/bin/PSFramework.pdb differ
diff --git a/PSFramework/bin/PSFramework.xml b/PSFramework/bin/PSFramework.xml
index 6820fa05..a0750440 100644
--- a/PSFramework/bin/PSFramework.xml
+++ b/PSFramework/bin/PSFramework.xml
@@ -1992,6 +1992,11 @@
Governs, whether a log of recent errors is kept in memory
+
+
+ Whether the filesystem logging provider uses the modern logging style with CSV headers and extra columns
+
+
The outbound queue for errors. These will be processed and written to xml
@@ -3574,6 +3579,18 @@
The path to where the module was imported from
+
+
+ Initializes the PSFramework library.
+ Required for some components to work correctly.
+
+
+
+
+ Reverses the initialization of the PSFramework library.
+ Should be called when destroying the main runspace
+
+
Attribute designating something as reserved as PSFramework-internal.
@@ -3653,6 +3670,35 @@
The value to offer or set, specific per runspace from which it is called
+
+
+ Removes all value entries whose corresponding Runspace has been destroyed
+
+
+
+
+ Destruction logic, eliminating all data stored in the object.
+ Since handles to this object are automatically stored and maintained, it is impossible to otherwise guarantee releasing the object's data for the GC.
+
+
+
+
+ Create an empty runspace bound value object
+
+
+
+
+ Create a runspace bound value object with its initial value
+
+ The object to set as the initial value
+
+
+
+ Create a runspace bound value object with its initial value
+
+ The object to set as the initial value
+ Whether the initial / default value should be offered when accessed from runspaces that do not have a runspace-local value
+
Class that contains the logic necessary to manage a unique runspace
@@ -3727,6 +3773,11 @@
The dictionary containing the definitive list of unique Runspace
+
+
+ List of all runspace bound values in use
+
+
Contains the state a managed, unique runspace can be in.
@@ -4000,6 +4051,26 @@
The cache used by scripts utilizing TabExpansionPlusPlus for PSFramework
+
+
+ Dictionary containing a list of hashtables to explicitly add properties when completing for specific output types.
+ Entries must have three properties:
+ - Name (Name of Property)
+ - Type (Type, not Typename, of the property. May be empty)
+ - TypeKnown (Boolean, whether the type is known)
+ Used by the Tab Completion: PSFramework-Input-ObjectProperty
+
+
+
+
+ Dictionary containing a list of hashtables to explicitly add properties when completing for specific commands
+ Entries must have three properties:
+ - Name (Name of Property)
+ - Type (Type, not Typename, of the property. May be empty)
+ - TypeKnown (Boolean, whether the type is known)
+ Used by the Tab Completion: PSFramework-Input-ObjectProperty
+
+
Whether the user wants to use simple tepp, full tepp or auto-detect
@@ -4679,6 +4750,12 @@
The current execution context
+
+
+ Returns the list of runspaces available in the process
+
+ The lists of currently known runspaces
+
Removes an alias from the global list of aliases
diff --git a/PSFramework/changelog.md b/PSFramework/changelog.md
index 1e6440d1..51fd9847 100644
--- a/PSFramework/changelog.md
+++ b/PSFramework/changelog.md
@@ -1,11 +1,13 @@
# CHANGELOG
## ???
- New: Configuration validation: Credential. Validates PSCredential objects.
+ - New: The most awesome Tab Completion for input properties _ever_ .
- Upd: Write-PSFMessage supports localized strings through the `-String` and `-StringValues` parameters
- Upd: Stop-PSFFunction supports localized strings through the `-String` and `-StringValues` parameters
- Upd: Test-PSFShouldProcess now supports ShouldProcess itself. This should help silence tests on commands reyling on it.
- Upd: Message component supports localized strings
- Upd: Logging component logs in separate language than localized messages to screen / userinteraction
+ - Upd: Logging - filesystem provider now has a configuration to enable better output information: `psframework.logging.filesystem.modernlog`
- Upd: Import-PSFLocalizedString now accepts wildcard path patterns that resovle to multiple files.
- Upd: Adding tab completion for `Register-PSFTeppArgumentCompleter`
- fix: Missing localization strings - Fix: Missing tab completion for modules that register localized strings
diff --git a/PSFramework/internal/configurations/logging.ps1 b/PSFramework/internal/configurations/logging.ps1
index 7f54c7b5..562354ad 100644
--- a/PSFramework/internal/configurations/logging.ps1
+++ b/PSFramework/internal/configurations/logging.ps1
@@ -1,7 +1,5 @@
-#region Setting the configuration
-Set-PSFConfig -Module PSFramework -Name 'Logging.MaxErrorCount' -Value 128 -Initialize -Validation "integerpositive" -Handler { [PSFramework.Message.LogHost]::MaxErrorCount = $args[0] } -Description "The maximum number of error records maintained in-memory. This setting is on a per-Process basis. Runspaces share, jobs or other consoles counted separately."
+Set-PSFConfig -Module PSFramework -Name 'Logging.MaxErrorCount' -Value 128 -Initialize -Validation "integerpositive" -Handler { [PSFramework.Message.LogHost]::MaxErrorCount = $args[0] } -Description "The maximum number of error records maintained in-memory. This setting is on a per-Process basis. Runspaces share, jobs or other consoles counted separately."
Set-PSFConfig -Module PSFramework -Name 'Logging.MaxMessageCount' -Value 1024 -Initialize -Validation "integerpositive" -Handler { [PSFramework.Message.LogHost]::MaxMessageCount = $args[0] } -Description "The maximum number of messages that can be maintained in the in-memory message queue. This setting is on a per-Process basis. Runspaces share, jobs or other consoles counted separately."
Set-PSFConfig -Module PSFramework -Name 'Logging.MessageLogEnabled' -Value $true -Initialize -Validation "bool" -Handler { [PSFramework.Message.LogHost]::MessageLogEnabled = $args[0] } -Description "Governs, whether a log of recent messages is kept in memory. This setting is on a per-Process basis. Runspaces share, jobs or other consoles counted separately."
Set-PSFConfig -Module PSFramework -Name 'Logging.ErrorLogEnabled' -Value $true -Initialize -Validation "bool" -Handler { [PSFramework.Message.LogHost]::ErrorLogEnabled = $args[0] } -Description "Governs, whether a log of recent errors is kept in memory. This setting is on a per-Process basis. Runspaces share, jobs or other consoles counted separately."
-Set-PSFConfig -Module PSFramework -Name 'Logging.DisableLogFlush' -Value $false -Initialize -Validation "bool" -Handler { } -Description "When shutting down the process, PSFramework will by default flush the log. This ensures that all events are properly logged. If this is not desired, it can be turned off with this setting."
-#endregion Setting the configuration
\ No newline at end of file
+Set-PSFConfig -Module PSFramework -Name 'Logging.DisableLogFlush' -Value $false -Initialize -Validation "bool" -Handler { } -Description "When shutting down the process, PSFramework will by default flush the log. This ensures that all events are properly logged. If this is not desired, it can be turned off with this setting."
\ No newline at end of file
diff --git a/PSFramework/internal/loggingProviders/filesystem.provider.ps1 b/PSFramework/internal/loggingProviders/filesystem.provider.ps1
index 1154372d..7b6ab35a 100644
--- a/PSFramework/internal/loggingProviders/filesystem.provider.ps1
+++ b/PSFramework/internal/loggingProviders/filesystem.provider.ps1
@@ -116,7 +116,15 @@ $message_Event = {
if ($Message)
{
- Add-Content -Path $filesystem_CurrentFile -Value (ConvertTo-Csv ($Message | Select-PSFObject ComputerName, Timestamp, Level, 'LogMessage as Message', Type, FunctionName, ModuleName, File, Line, @{ n = "Tags"; e = { $_.Tags -join "," } }, TargetObject, Runspace) -NoTypeInformation)[1]
+ if ([PSFramework.Message.LogHost]::FileSystemModernLog)
+ {
+ if (-not (Test-Path $filesystem_CurrentFile))
+ {
+ $Message | Select-PSFObject ComputerName, Username, Timestamp, Level, 'LogMessage as Message', Type, FunctionName, ModuleName, File, Line, @{ n = "Tags"; e = { $_.Tags -join "," } }, TargetObject, Runspace, @{ n = "Callstack"; e = { $_.CallStack.ToString().Split("`n") -join " þ "} } | Export-Csv -Path $filesystem_CurrentFile -NoTypeInformation
+ }
+ else { Add-Content -Path $filesystem_CurrentFile -Value (ConvertTo-Csv ($Message | Select-PSFObject ComputerName, Username, Timestamp, Level, 'LogMessage as Message', Type, FunctionName, ModuleName, File, Line, @{ n = "Tags"; e = { $_.Tags -join "," } }, TargetObject, Runspace, @{ n = "Callstack"; e = { $_.CallStack.ToString().Split("`n") -join " þ " } }) -NoTypeInformation)[1] }
+ }
+ else { Add-Content -Path $filesystem_CurrentFile -Value (ConvertTo-Csv ($Message | Select-PSFObject ComputerName, Timestamp, Level, 'LogMessage as Message', Type, FunctionName, ModuleName, File, Line, @{ n = "Tags"; e = { $_.Tags -join "," } }, TargetObject, Runspace) -NoTypeInformation)[1] }
}
}
@@ -209,6 +217,7 @@ $configuration_Settings = {
Set-PSFConfig -Module PSFramework -Name 'Logging.FileSystem.MaxLogFileAge' -Value (New-TimeSpan -Days 7) -Initialize -Validation "timespan" -Handler { [PSFramework.Message.LogHost]::MaxLogFileAge = $args[0] } -Description "Any logfile older than this will automatically be cleansed. This setting is global."
Set-PSFConfig -Module PSFramework -Name 'Logging.FileSystem.MessageLogFileEnabled' -Value $true -Initialize -Validation "bool" -Handler { [PSFramework.Message.LogHost]::MessageLogFileEnabled = $args[0] } -Description "Governs, whether a log file for the system messages is written. This setting is on a per-Process basis. Runspaces share, jobs or other consoles counted separately."
Set-PSFConfig -Module PSFramework -Name 'Logging.FileSystem.ErrorLogFileEnabled' -Value $true -Initialize -Validation "bool" -Handler { [PSFramework.Message.LogHost]::ErrorLogFileEnabled = $args[0] } -Description "Governs, whether log files for errors are written. This setting is on a per-Process basis. Runspaces share, jobs or other consoles counted separately."
+ Set-PSFConfig -Module PSFramework -Name 'Logging.FileSystem.ModernLog' -Value $false -Initialize -Validation "bool" -Handler { [PSFramework.Message.LogHost]::FileSystemModernLog = $args[0] } -Description "Enables the modern, more powereful version of the filesystem log, including headers and extra columns"
Set-PSFConfig -Module PSFramework -Name 'Logging.FileSystem.LogPath' -Value $script:path_Logging -Initialize -Validation "string" -Handler { [PSFramework.Message.LogHost]::LoggingPath = $args[0] } -Description "The path where the PSFramework writes all its logs and debugging information."
Set-PSFConfig -Module LoggingProvider -Name 'FileSystem.Enabled' -Value $true -Initialize -Validation "bool" -Handler { if ([PSFramework.Logging.ProviderHost]::Providers['filesystem']) { [PSFramework.Logging.ProviderHost]::Providers['filesystem'].Enabled = $args[0] } } -Description "Whether the logging provider should be enabled on registration"
diff --git a/PSFramework/internal/scripts/postimport.ps1 b/PSFramework/internal/scripts/postimport.ps1
index 61cab441..5da6e783 100644
--- a/PSFramework/internal/scripts/postimport.ps1
+++ b/PSFramework/internal/scripts/postimport.ps1
@@ -48,8 +48,11 @@ foreach ($file in (Get-ChildItem -Path "$($script:ModuleRoot)\internal\parameter
# Register the unimport reaction
. Import-ModuleFile -Path "$($script:ModuleRoot)\internal\scripts\removalEvent.ps1"
-# Load specialvariables
+# Load special variables
. Import-ModuleFile -Path "$($script:ModuleRoot)\internal\scripts\variables.ps1"
+# Load resources for TEPP input completion
+. Import-ModuleFile -Path "$($script:ModuleRoot)\internal\scripts\teppInputResources.ps1"
+
# Finally register the license
. Import-ModuleFile -Path "$($script:ModuleRoot)\internal\scripts\license.ps1"
\ No newline at end of file
diff --git a/PSFramework/internal/scripts/teppInputResources.ps1 b/PSFramework/internal/scripts/teppInputResources.ps1
new file mode 100644
index 00000000..30c1c129
--- /dev/null
+++ b/PSFramework/internal/scripts/teppInputResources.ps1
@@ -0,0 +1,80 @@
+[PSFramework.TabExpansion.TabExpansionHost]::InputCompletionTypeData['System.IO.FileInfo'] = @(
+ [PSCustomObject]@{
+ Name = 'PSChildName'
+ Type = ([type]'System.String')
+ TypeKnown = $true
+ },
+ [PSCustomObject]@{
+ Name = 'PSDrive'
+ Type = ([type]'System.Management.Automation.PSDriveInfo')
+ TypeKnown = $true
+ },
+ [PSCustomObject]@{
+ Name = 'PSIsContainer'
+ Type = ([type]'System.Boolean')
+ TypeKnown = $true
+ },
+ [PSCustomObject]@{
+ Name = 'PSParentPath'
+ Type = ([type]'System.String')
+ TypeKnown = $true
+ },
+ [PSCustomObject]@{
+ Name = 'PSPath'
+ Type = ([type]'System.String')
+ TypeKnown = $true
+ },
+ [PSCustomObject]@{
+ Name = 'PSProvider'
+ Type = ([type]'System.Management.Automation.ProviderInfo')
+ TypeKnown = $true
+ },
+ [PSCustomObject]@{
+ Name = 'BaseName'
+ Type = ([type]'System.String')
+ TypeKnown = $true
+ },
+ [PSCustomObject]@{
+ Name = 'VersionInfo'
+ Type = ([type]'System.Diagnostics.FileVersionInfo')
+ TypeKnown = $true
+ }
+)
+
+[PSFramework.TabExpansion.TabExpansionHost]::InputCompletionTypeData['System.IO.DirectoryInfo'] = @(
+ [PSCustomObject]@{
+ Name = 'PSChildName'
+ Type = ([type]'System.String')
+ TypeKnown = $true
+ },
+ [PSCustomObject]@{
+ Name = 'PSDrive'
+ Type = ([type]'System.Management.Automation.PSDriveInfo')
+ TypeKnown = $true
+ },
+ [PSCustomObject]@{
+ Name = 'PSIsContainer'
+ Type = ([type]'System.Boolean')
+ TypeKnown = $true
+ },
+ [PSCustomObject]@{
+ Name = 'PSParentPath'
+ Type = ([type]'System.String')
+ TypeKnown = $true
+ },
+ [PSCustomObject]@{
+ Name = 'PSPath'
+ Type = ([type]'System.String')
+ TypeKnown = $true
+ },
+ [PSCustomObject]@{
+ Name = 'PSProvider'
+ Type = ([type]'System.Management.Automation.ProviderInfo')
+ TypeKnown = $true
+ },
+ [PSCustomObject]@{
+ Name = 'BaseName'
+ Type = ([type]'System.String')
+ TypeKnown = $true
+ }
+)
\ No newline at end of file
diff --git a/PSFramework/internal/tepp/scripts/input.ps1 b/PSFramework/internal/tepp/scripts/input.ps1
index 38f5cb8a..55acbefa 100644
--- a/PSFramework/internal/tepp/scripts/input.ps1
+++ b/PSFramework/internal/tepp/scripts/input.ps1
@@ -1,163 +1,710 @@
-Register-PSFTeppScriptblock -Name PSFramework-Input-Object -ScriptBlock {
- [System.Management.Automation.Language.PipelineAst]$pipelineAst = $commandAst.parent
- $index = $pipelineAst.PipelineElements.IndexOf($commandAst)
-
- #region If it's the first command
- if ($index -lt 1)
+Register-PSFTeppScriptblock -Name PSFramework-Input-ObjectProperty -ScriptBlock {
+ #region Utility Functions
+ function Get-Property
{
- return
+ [CmdletBinding()]
+ param (
+ $InputObject
+ )
+
+ if (-not $InputObject) { return @{ } }
+ $properties = @{ }
+
+ switch ($InputObject.GetType().FullName)
+ {
+ #region Variables or static input
+ 'System.Management.Automation.Language.CommandExpressionAst'
+ {
+ switch ($InputObject.Expression.GetType().Name)
+ {
+ 'BinaryExpressionAst'
+ {
+ # Return an empty array. A binary expression ast means pure numbers as input, no properties
+ return @{ }
+ }
+ 'VariableExpressionAst'
+ {
+ $members = Get-Variable -Name $InputObject.Expression.VariablePath.UserPath -ValueOnly -ErrorAction Ignore | Select-Object -First 1 | Get-Member -MemberType Properties
+ foreach ($member in $members)
+ {
+ try
+ {
+ $typeString = $member.Definition.Split(" ")[0]
+ $memberType = [type]$typeString
+ $typeKnown = $true
+ }
+ catch
+ {
+ $memberType = $null
+ $typeKnown = $false
+ }
+
+ $properties[$member.Name] = [pscustomobject]@{
+ Name = $member.Name
+ Type = $memberType
+ TypeKnown = $typeKnown
+ }
+ }
+ return $properties
+ }
+ 'MemberExpressionAst'
+ {
+ try { $members = Get-Variable -Name $InputObject.Expression.Expression.VariablePath.UserPath -ValueOnly -ErrorAction Ignore | Where-Object $InputObject.Expression.Member.Value -ne $null | Select-Object -First 1 -ExpandProperty $InputObject.Expression.Member.Value -ErrorAction Ignore | Get-Member -MemberType Properties }
+ catch { return $properties }
+ foreach ($member in $members)
+ {
+ try
+ {
+ $typeString = $member.Definition.Split(" ")[0]
+ $memberType = [type]$typeString
+ $typeKnown = $true
+ }
+ catch
+ {
+ $memberType = $null
+ $typeKnown = $false
+ }
+
+ $properties[$member.Name] = [pscustomobject]@{
+ Name = $member.Name
+ Type = $memberType
+ TypeKnown = $typeKnown
+ }
+ }
+ return $properties
+ }
+ 'ArrayLiteralAst'
+ {
+ # Not yet supported
+ return @{ }
+ }
+ }
+ #region Input from Variable
+ if ($pipelineAst.PipelineElements[$inputIndex].Expression -and $pipelineAst.PipelineElements[0].Expression[0].VariablePath)
+ {
+ $properties += ((Get-Variable -Name $pipelineAst.PipelineElements[0].Expression[0].VariablePath.UserPath -ValueOnly) | Select-Object -First 1 | Get-Member -MemberType Properties).Name
+ }
+ #endregion Input from Variable
+ }
+ #endregion Variables or static input
+
+ #region Input from Command
+ 'System.Management.Automation.Language.CommandAst'
+ {
+ $command = Get-Command $InputObject.CommandElements[0].Value -ErrorAction Ignore
+ if ($command -is [System.Management.Automation.AliasInfo]) { $command = $command.ResolvedCommand }
+ if (-not $command) { return $properties }
+
+ foreach ($type in $command.OutputType.Type)
+ {
+ foreach ($member in $type.GetMembers("Instance, Public"))
+ {
+ # Skip all members except Fields (4) or Properties (16)
+ if (-not ($member.MemberType -band 20)) { continue }
+
+ $properties[$member.Name] = [pscustomobject]@{
+ Name = $member.Name
+ Type = $null
+ TypeKnown = $true
+ }
+ if ($member.PropertyType) { $properties[$member.Name].Type = $member.PropertyType }
+ else { $properties[$member.Name].Type = $member.FieldType }
+ }
+
+ foreach ($propertyExtensionItem in ([PSFramework.TabExpansion.TabExpansionHost]::InputCompletionTypeData[$type.FullName]))
+ {
+ $properties[$propertyExtensionItem.Name] = $propertyExtensionItem
+ }
+ }
+
+ #region Command Specific Inserts
+ foreach ($propertyExtensionItem in ([PSFramework.TabExpansion.TabExpansionHost]::InputCompletionCommandData[$command.Name]))
+ {
+ $properties[$propertyExtensionItem.Name] = $propertyExtensionItem
+ }
+ #endregion Command Specific Inserts
+
+ return $properties
+ }
+ #endregion Input from Command
+
+ # Unknown / Unexpected input
+ default { return @{ } }
+ }
}
- #endregion If it's the first command
-
- $properties = @()
- $constraintsPositive = @()
- #region Process pre-commands
- $inputIndex = $index - 1
- :main while ($true)
+ function Update-Property
{
- if ($pipelineAst.PipelineElements[$inputIndex].CommandElements)
+ [CmdletBinding()]
+ param (
+ [Hashtable]
+ $Property,
+
+ $Step
+ )
+
+ $properties = @{ }
+ #region Expand Property
+ if ($Step.ExpandProperty)
{
- # Resolve command and fail if it breaks
- $commandString = $pipelineAst.PipelineElements[$inputIndex].CommandElements[0].Value
- if ($commandString -eq "?") { $commandString = (Get-Alias -Name "?").ResolvedCommand.Name }
- $command = Get-Command $commandString -ErrorAction Ignore
- if ($command -is [System.Management.Automation.AliasInfo]) { $command = $command.ResolvedCommand }
- if (-not $command) { break }
+ if (-not ($Property[$Step.ExpandProperty])) { return $properties }
- switch ($command.Name)
+ $expanded = $Property[$Step.ExpandProperty]
+ if (-not $expanded.TypeKnown) { return $properties }
+
+ foreach ($member in $expanded.Type.GetMembers("Instance, Public"))
{
- 'Where-Object' { $inputIndex = $inputIndex - 1; continue main }
- 'Tee-Object' { $inputIndex = $inputIndex - 1; continue main }
- 'Sort-Object' { $inputIndex = $inputIndex - 1; continue main }
- #region Select-Object
- 'Select-Object'
+ # Skip all members except Fields (4) or Properties (16)
+ if (-not ($member.MemberType -band 20)) { continue }
+
+ $properties[$member.Name] = [pscustomobject]@{
+ Name = $member.Name
+ Type = $null
+ TypeKnown = $true
+ }
+ if ($member.PropertyType) { $properties[$member.Name].Type = $member.PropertyType }
+ else { $properties[$member.Name].Type = $member.FieldType }
+ }
+
+ foreach ($propertyExtensionItem in ([PSFramework.TabExpansion.TabExpansionHost]::InputCompletionTypeData[$expanded.Type.FullName]))
+ {
+ $properties[$propertyExtensionItem.Name] = $propertyExtensionItem
+ }
+
+ return $properties
+ }
+ #endregion Expand Property
+
+ # In keep input mode, the original properties will not be affected in any way
+ if ($Step.KeepInputObject) { $properties = $Property.Clone() }
+ $filterProperties = $Step.Properties | Where-Object Kind -eq "Property"
+
+ #region Select What to keep
+ if (-not $Step.KeepInputObject)
+ {
+ :main foreach ($propertyItem in $Property.Values)
+ {
+ #region Excluded Properties
+ foreach ($exclusion in $Step.Excluded)
+ {
+ if ($propertyItem.Name -like $exclusion) { continue main }
+ }
+ #endregion Excluded Properties
+
+ foreach ($stepProperty in $filterProperties)
+ {
+ if ($propertyItem.Name -like $stepProperty.Name)
+ {
+ $properties[$propertyItem.Name] = $propertyItem
+ continue main
+ }
+ }
+ }
+ }
+ #endregion Select What to keep
+
+ #region Adding Content
+ :main foreach ($stepProperty in $Step.Properties)
+ {
+ switch ($stepProperty.Kind)
+ {
+ 'Property'
{
- $firstAst = $pipelineAst.PipelineElements[$inputIndex].CommandElements | Where-Object { $_ -is [System.Management.Automation.Language.ArrayLiteralAst] } | Select-Object -First 1
+ if ($stepProperty.Filter) { continue main }
+ if ($properties[$stepProperty.Name]) { continue main }
- foreach ($element in $firstAst.Elements)
+ foreach ($exclusion in $Step.Excluded)
{
- switch ($element.GetType().FullName)
+ if ($stepProperty.Name -like $exclusion) { continue main }
+ }
+
+ $properties[$stepProperty.Name] = [PSCustomObject]@{
+ Name = $stepProperty.Name
+ Type = $null
+ TypeKnown = $false
+ }
+ continue main
+ }
+ 'CalculatedProperty'
+ {
+ if ($properties[$stepProperty.Name]) { continue main }
+
+ $properties[$stepProperty.Name] = [PSCustomObject]@{
+ Name = $stepProperty.Name
+ Type = $null
+ TypeKnown = $false
+ }
+ continue main
+ }
+ 'ScriptProperty'
+ {
+ if ($properties[$stepProperty.Name]) { continue main }
+
+ $properties[$stepProperty.Name] = [PSCustomObject]@{
+ Name = $stepProperty.Name
+ Type = $null
+ TypeKnown = $false
+ }
+ continue main
+ }
+ 'AliasProperty'
+ {
+ if ($properties[$stepProperty.Name]) { continue main }
+
+ $properties[$stepProperty.Name] = [PSCustomObject]@{
+ Name = $stepProperty.Name
+ Type = $null
+ TypeKnown = $false
+ }
+ if ($properties[$stepProperty.Target].TypeKnown)
+ {
+ $properties[$stepProperty.Name].Type = $properties[$stepProperty.Target].Type
+ $properties[$stepProperty.Name].TypeKnown = $properties[$stepProperty.Target].TypeKnown
+ }
+
+ continue main
+ }
+ }
+ }
+ #endregion Adding Content
+ $properties
+ }
+
+ function Read-SelectObject
+ {
+ [CmdletBinding()]
+ param (
+ [System.Management.Automation.Language.CommandAst]
+ $Ast,
+
+ [string]
+ $CommandName = 'Select-Object'
+ )
+
+ $results = [pscustomobject]@{
+ Ast = $Ast
+ BoundParameters = @()
+ Property = @()
+ ExcludeProperty = @()
+ ExpandProperty = ''
+ ScriptProperty = @()
+ AliasProperty = @()
+ KeepInputObject = $false
+ }
+
+ #region Process Ast
+ if ($Ast.CommandElements.Count -gt 1)
+ {
+ $index = 1
+ $parameterName = ''
+ $position = 0
+ while ($index -lt $Ast.CommandElements.Count)
+ {
+ $element = $Ast.CommandElements[$index]
+ switch ($element.GetType().FullName)
+ {
+ 'System.Management.Automation.Language.CommandParameterAst'
+ {
+ $parameterName = $element.ParameterName
+ if ($parameterName -like "k*") { $results.KeepInputObject = $true }
+ $results.BoundParameters += $element.ParameterName
+ break
+ }
+ 'System.Management.Automation.Language.StringConstantExpressionAst'
+ {
+ if (-not $parameterName)
{
- 'System.Management.Automation.Language.StringConstantExpressionAst'
+ switch ($position)
{
- $constraintsPositive += $element.Value
- if ($element.Value -notmatch "\*") { $properties += $element.Value }
+ 0 { $results.Property = $element }
+ 1 { $results.AliasProperty = $element }
+ 2 { $results.ScriptProperty = $element }
}
- 'System.Management.Automation.Language.HashtableAst'
+ $position = $position + 1
+ }
+
+ if ($parameterName -like "pr*") { $results.Property = $element }
+ if ($parameterName -like "exp*") { $results.ExpandProperty = $element.Value }
+ if ($parameterName -like "exc*") { $results.ExcludeProperty = $element.Value }
+ if ($parameterName -like "a*") { $results.AliasProperty = $element }
+ if ($parameterName -like "scriptp*") { $results.ScriptProperty = $element }
+ $parameterName = ''
+ break
+ }
+ 'System.Management.Automation.Language.ArrayLiteralAst'
+ {
+ if (-not $parameterName)
+ {
+ switch ($position)
{
- $constraintsPositive += ($element.KeyValuePairs | Where-Object Item1 -Match '^N$|^Name$' | Select-Object -First 1).Item2.ToString().Trim('"')
- $properties += ($element.KeyValuePairs | Where-Object Item1 -Match '^N$|^Name$' | Select-Object -First 1).Item2.ToString().Trim('"')
+ 0 { $results.Property = $element.Elements }
+ 1 { $results.AliasProperty = $element.Elements }
+ 2 { $results.ScriptProperty = $element.Elements }
}
+ $position = $position + 1
}
+
+ if ($parameterName -like "pr*") { $results.Property = $element.Elements }
+ if ($parameterName -like "exp*") { $results.ExpandProperty = $element.Elements.Value }
+ if ($parameterName -like "exc*") { $results.ExcludeProperty = $element.Elements.Value }
+ if ($parameterName -like "a*") { $results.AliasProperty = $element.Elements }
+ if ($parameterName -like "scriptp*") { $results.ScriptProperty = $element.Elements }
+
+ $parameterName = ''
+ break
}
- $inputIndex = $inputIndex - 1;
- continue main
- }
- #endregion Select-Object
- #region Select-PSFObject
- 'Select-PSFObject'
- {
- $firstAst = $pipelineAst.PipelineElements[$inputIndex].CommandElements | Where-Object { $_ -is [System.Management.Automation.Language.ArrayLiteralAst] } | Select-Object -First 1
- foreach ($element in $firstAst.Elements)
+ 'System.Management.Automation.Language.ConstantExpressionAst'
{
- switch ($element.GetType().FullName)
+ if (-not $parameterName)
{
- "System.Management.Automation.Language.StringConstantExpressionAst"
+ switch ($position)
{
- $par = [PSFramework.Parameter.SelectParameter]$element.Value
- if ($par.Value -match "\*") { $constraintsPositive += $par.Value }
- else
- {
- if ($par.Value -is [System.String])
- {
- $properties += $par.Value
- $constraintsPositive += $par.Value
- }
- else
- {
- $properties += $par.Value["Name"]
- $constraintsPositive += $par.Value["Name"]
- }
- }
+ 0 { $results.Property = $element }
+ 1 { $results.AliasProperty = $element }
+ 2 { $results.ScriptProperty = $element }
}
- "System.Management.Automation.Language.HashtableAst"
+ $position = $position + 1
+ }
+
+ if ($parameterName -like "pr*") { $results.Property = $element }
+ if ($parameterName -like "exp*") { $results.ExpandProperty = $element.Value.ToString() }
+ if ($parameterName -like "exc*") { $results.ExcludeProperty = $element.Value.ToString() }
+ if ($parameterName -like "a*") { $results.AliasProperty = $element }
+ if ($parameterName -like "scriptp*") { $results.ScriptProperty = $element }
+ $parameterName = ''
+ break
+ }
+ 'System.Management.Automation.Language.HashtableAst'
+ {
+ if (-not $parameterName)
+ {
+ switch ($position)
{
- $properties += ($element.KeyValuePairs | Where-Object Item1 -Match '^N$|^Name$' | Select-Object -First 1).Item2.ToString().Trim('"')
- $constraintsPositive += ($element.KeyValuePairs | Where-Object Item1 -Match '^N$|^Name$' | Select-Object -First 1).Item2.ToString().Trim('"')
+ 0 { $results.Property = $element }
+ 1 { $results.AliasProperty = $element }
+ 2 { $results.ScriptProperty = $element }
}
+ $position = $position + 1
}
+
+ if ($parameterName -like "pr*") { $results.Property = $element }
+ if ($parameterName -like "a*") { $results.AliasProperty = $element }
+ if ($parameterName -like "scriptp*") { $results.ScriptProperty = $element }
+ $parameterName = ''
+ break
+ }
+ default
+ {
+ $parameterName = ''
}
- $inputIndex = $inputIndex - 1;
}
- #endregion Select-PSFObject
- default { break main }
+ $index = $index + 1
}
}
+ #endregion Process Ast
- else
- {
- break
+ #region Convert Results
+ $resultsProcessed = [pscustomobject]@{
+ HasIncludeFilter = $false
+ RawResult = $results
+ Properties = @()
+ Excluded = $results.ExcludeProperty
+ ExpandProperty = $results.ExpandProperty
+ KeepInputObject = $results.KeepInputObject
}
- }
-
- # Catch moving through _all_ options in the pipeline
- if ($inputIndex -lt 0) { return $properties }
- #endregion Process pre-commands
-
-
- #region Input from command
- if ($pipelineAst.PipelineElements[$inputIndex].CommandElements)
- {
- if ($command = Get-Command $pipelineAst.PipelineElements[$inputIndex].CommandElements[0].Value -ErrorAction Ignore)
+
+ switch ($CommandName)
{
- switch ($command.Name)
+ #region Select-Object
+ 'Select-Object'
{
- #region Default for commands
- default
+ #region Properties
+ foreach ($element in $results.Property)
{
- foreach ($type in $command.OutputType.Type)
+ switch ($element.GetType().FullName)
{
- switch ($type.FullName)
+ 'System.Management.Automation.Language.HashtableAst'
{
- 'System.IO.FileInfo'
+ try
{
- $properties += ($type.GetMembers("Instance, Public") | Where-Object MemberType -match "Field|Property").Name
- $properties += 'PSChildName', 'PSDrive', 'PSIsContainer', 'PSParentPath', 'PSPath', 'PSProvider', 'BaseName'
- break
+ $resultsProcessed.Properties += [pscustomobject]@{
+ Name = ($element.KeyValuePairs | Where-Object Item1 -Match '^N$|^Name$|^L$|^Label$' | Select-Object -First 1).Item2.PipelineElements[0].Expression.Value
+ Kind = "CalculatedProperty"
+ Type = "Unknown"
+ Filter = $false
+ }
}
- 'System.IO.DirectoryInfo'
+ catch { }
+ }
+ default
+ {
+ if ($element.Value -match "\*") { $resultsProcessed.HasIncludeFilter = $true }
+
+ $resultsProcessed.Properties += [pscustomobject]@{
+ Name = $element.Value.ToString()
+ Kind = "Property"
+ Type = "Inherited"
+ Filter = $element.Value -match "\*"
+ }
+ }
+ }
+ }
+ #endregion Properties
+ }
+ #endregion Select-Object
+
+ #region Select-PSFObject
+ 'Select-PSFObject'
+ {
+ #region Properties
+ foreach ($element in $results.Property)
+ {
+ switch ($element.GetType().FullName)
+ {
+ 'System.Management.Automation.Language.HashtableAst'
+ {
+ try
+ {
+ $resultsProcessed.Properties += [pscustomobject]@{
+ Name = ($element.KeyValuePairs | Where-Object Item1 -Match '^N$|^Name$|^L$|^Label$' | Select-Object -First 1).Item2.PipelineElements[0].Expression.Value
+ Kind = "CalculatedProperty"
+ Type = "Unknown"
+ Filter = $false
+ }
+ }
+ catch { }
+ }
+ default
+ {
+ try { $parameterItem = ([PSFramework.Parameter.SelectParameter]$element.Value).Value }
+ catch { continue }
+
+ if ($parameterItem -is [System.String])
+ {
+ if ($parameterItem -match "\*") { $resultsProcessed.HasIncludeFilter = $true }
+
+ $resultsProcessed.Properties += [pscustomobject]@{
+ Name = $parameterItem
+ Kind = "Property"
+ Type = "Inherited"
+ Filter = $parameterItem -match "\*"
+ }
+ }
+ else
+ {
+ $resultsProcessed.Properties += [pscustomobject]@{
+ Name = $parameterItem
+ Kind = "CalculatedProperty"
+ Type = "Unknown"
+ Filter = $false
+ }
+ }
+ }
+ }
+ }
+ #endregion Properties
+
+ #region Script Properties
+ foreach ($scriptProperty in $results.ScriptProperty)
+ {
+ switch ($scriptProperty.GetType().FullName)
+ {
+ 'System.Management.Automation.Language.HashtableAst'
+ {
+ foreach ($name in $scriptProperty.KeyValuePairs.Item1.Value)
+ {
+ $resultsProcessed.Properties += [pscustomobject]@{
+ Name = $name
+ Kind = "ScriptProperty"
+ Type = "Unknown"
+ Filter = $false
+ }
+ }
+ }
+ default
+ {
+ try { $propertyValue = [PSFramework.Parameter.SelectScriptPropertyParameter]$scriptProperty.Value }
+ catch { continue }
+
+ $resultsProcessed.Properties += [pscustomobject]@{
+ Name = $propertyValue.Value.Name
+ Kind = "ScriptProperty"
+ Type = "Unknown"
+ Filter = $false
+ }
+ }
+ }
+ }
+ #endregion Script Properties
+
+ #region Alias Properties
+ foreach ($scriptProperty in $results.AliasProperty)
+ {
+ switch ($scriptProperty.GetType().FullName)
+ {
+ 'System.Management.Automation.Language.HashtableAst'
+ {
+ foreach ($aliasPair in $scriptProperty.KeyValuePairs)
{
- $properties += ($type.GetMembers("Instance, Public") | Where-Object MemberType -match "Field|Property").Name
- $properties += 'PSChildName', 'PSDrive', 'PSIsContainer', 'PSParentPath', 'PSPath', 'PSProvider', 'BaseName', 'VersionInfo'
- break
+ $resultsProcessed.Properties += [pscustomobject]@{
+ Name = $aliasPair.Item1.Value
+ Kind = "AliasProperty"
+ Type = "Alias"
+ Filter = $false
+ Target = $aliasPair.Item2.PipelineElements.Expression.Value
+ }
+ }
+ }
+ default
+ {
+ try { $propertyValue = [PSFramework.Parameter.SelectAliasParameter]$scriptProperty.Value }
+ catch { continue }
+
+ $resultsProcessed.Properties += [pscustomobject]@{
+ Name = $propertyValue.Aliases[0].Name
+ Kind = "AliasProperty"
+ Type = "Alias"
+ Filter = $false
+ Target = $propertyValue.Aliases[0].ReferencedMemberName
}
- default { $properties += ($type.GetMembers("Instance, Public") | Where-Object MemberType -match "Field|Property").Name }
}
}
}
- #endregion Default for commands
+ #endregion Alias Properties
}
+ #endregion Select-PSFObject
}
+ #endregion Convert Results
+
+ $resultsProcessed
}
- #endregion Input from command
+ #endregion Utility Functions
- #region Input from Variable
- if ($pipelineAst.PipelineElements[$inputIndex].Expression -and $pipelineAst.PipelineElements[0].Expression[0].VariablePath)
- {
- $properties += ((Get-Variable -Name $pipelineAst.PipelineElements[0].Expression[0].VariablePath.UserPath -ValueOnly) | Select-Object -First 1 | Get-Member -MemberType Properties).Name
- }
- #endregion Input from Variable
+ # Grab Pipeline and find starting index
+ [System.Management.Automation.Language.PipelineAst]$pipelineAst = $commandAst.parent
+ $index = $pipelineAst.PipelineElements.IndexOf($commandAst)
+
+ # If it's the first item: Skip, no input to parse
+ if ($index -lt 1) { return }
- $properties | Select-Object -Unique | Sort-Object | ForEach-Object {
- if (-not $constraintsPositive) { $_ }
- foreach ($constraint in $constraintsPositive)
+ $inputIndex = $index - 1
+ $steps = @{ }
+
+ #region Step backwards through the pipeline until the definitive object giver is found
+ :outmain while ($true)
+ {
+ if ($pipelineAst.PipelineElements[$inputIndex].CommandElements)
{
- if ($_ -like $constraint)
+ # Resolve command and fail if it breaks
+ $command = $null
+ # Work around the ? alias for Where-Object being a wildcard
+ if ($pipelineAst.PipelineElements[$inputIndex].CommandElements[0].Value -eq "?") { $command = Get-Alias -Name "?" | Where-Object Name -eq "?" }
+ else { $command = Get-Command $pipelineAst.PipelineElements[$inputIndex].CommandElements[0].Value -ErrorAction Ignore }
+ if ($command -is [System.Management.Automation.AliasInfo]) { $command = $command.ResolvedCommand }
+ if (-not $command) { return }
+
+ switch ($command.Name)
{
- $_
- break
+ 'Where-Object'
+ {
+ $steps[$inputIndex] = [pscustomobject]@{
+ Index = $inputIndex
+ Skip = $true
+ Type = 'Where'
+ }
+ $inputIndex = $inputIndex - 1
+ continue outmain
+ }
+ 'Tee-Object'
+ {
+ $steps[$inputIndex] = [pscustomobject]@{
+ Index = $inputIndex
+ Skip = $true
+ Type = 'Tee'
+ }
+ $inputIndex = $inputIndex - 1
+ continue outmain
+ }
+ 'Sort-Object'
+ {
+ $steps[$inputIndex] = [pscustomobject]@{
+ Index = $inputIndex
+ Skip = $true
+ Type = 'Sort'
+ }
+ $inputIndex = $inputIndex - 1
+ continue outmain
+ }
+ #region Select-Object
+ 'Select-Object'
+ {
+ $selectObject = Read-SelectObject -Ast $pipelineAst.PipelineElements[$inputIndex] -CommandName 'Select-Object'
+
+ $steps[$inputIndex] = [pscustomobject]@{
+ Index = $inputIndex
+ Skip = $false
+ Type = 'Select'
+ Data = $selectObject
+ }
+
+ if ($selectObject.HasIncludeFilter -or ($selectObject.Properties.Type -eq "Inherited") -or $selectObject.ExpandProperty)
+ {
+ $inputIndex = $inputIndex - 1
+ continue outmain
+ }
+ break outmain
+ }
+ #endregion Select-Object
+ #region Select-PSFObject
+ 'Select-PSFObject'
+ {
+ $selectObject = Read-SelectObject -Ast $pipelineAst.PipelineElements[$inputIndex] -CommandName 'Select-PSFObject'
+
+ $steps[$inputIndex] = [pscustomobject]@{
+ Index = $inputIndex
+ Skip = $false
+ Type = 'PSFSelect'
+ Data = $selectObject
+ }
+
+ if ($selectObject.HasIncludeFilter -or ($selectObject.Properties.Type -eq "Inherited") -or $selectObject.ExpandProperty)
+ {
+ $inputIndex = $inputIndex - 1
+ continue outmain
+ }
+ break outmain
+ }
+ #endregion Select-PSFObject
+ default { break outmain }
}
}
+
+ else
+ {
+ break
+ }
}
+ #endregion Step backwards through the pipeline until the definitive object giver is found
+
+ # Catch moving through _all_ options in the pipeline
+ if ($inputIndex -lt 0) { return }
+
+ #region Process resulting / reaching properties
+ $properties = Get-Property -InputObject $pipelineAst.PipelineElements[$inputIndex]
+ $inputIndex = $inputIndex + 1
+
+ while ($inputIndex -lt $index)
+ {
+ # Eliminate preliminary follies
+ if (-not $steps[$inputIndex]) { $inputIndex = $inputIndex + 1; continue }
+ if ($steps[$inputIndex].Skip) { $inputIndex = $inputIndex + 1; continue }
+
+ # Process the current step, then move on unless done
+ $properties = Update-Property -Property $properties -Step $steps[$inputIndex].Data
+
+ $inputIndex = $inputIndex + 1
+ }
+ #endregion Process resulting / reaching properties
+
+ $properties.Keys | Sort-Object
}
\ No newline at end of file
diff --git a/PSFramework/internal/tepp/tepp-assignment.ps1 b/PSFramework/internal/tepp/tepp-assignment.ps1
index 543fb496..ba350bde 100644
--- a/PSFramework/internal/tepp/tepp-assignment.ps1
+++ b/PSFramework/internal/tepp/tepp-assignment.ps1
@@ -68,11 +68,11 @@ Register-PSFTeppArgumentCompleter -Command Register-PSFTeppArgumentCompleter -Pa
#endregion Tab Completion
#region Utility
-Register-PSFTeppArgumentCompleter -Command Select-PSFObject -Parameter Property -Name PSFramework-Input-Object
-Register-PSFTeppArgumentCompleter -Command Select-PSFObject -Parameter ExpandProperty -Name PSFramework-Input-Object
-Register-PSFTeppArgumentCompleter -Command Select-PSFObject -Parameter ExcludeProperty -Name PSFramework-Input-Object
-Register-PSFTeppArgumentCompleter -Command Select-PSFObject -Parameter ShowProperty -Name PSFramework-Input-Object
-Register-PSFTeppArgumentCompleter -Command Select-PSFObject -Parameter ShowExcludeProperty -Name PSFramework-Input-Object
+Register-PSFTeppArgumentCompleter -Command Select-PSFObject -Parameter Property -Name PSFramework-Input-ObjectProperty
+Register-PSFTeppArgumentCompleter -Command Select-PSFObject -Parameter ExpandProperty -Name PSFramework-Input-ObjectProperty
+Register-PSFTeppArgumentCompleter -Command Select-PSFObject -Parameter ExcludeProperty -Name PSFramework-Input-ObjectProperty
+Register-PSFTeppArgumentCompleter -Command Select-PSFObject -Parameter ShowProperty -Name PSFramework-Input-ObjectProperty
+Register-PSFTeppArgumentCompleter -Command Select-PSFObject -Parameter ShowExcludeProperty -Name PSFramework-Input-ObjectProperty
Register-PSFTeppArgumentCompleter -Command Resolve-PSFPath -Parameter Provider -Name 'PSFramework-utility-psprovider'
#endregion Utility
\ No newline at end of file
diff --git a/build/filesAfter.txt b/build/filesAfter.txt
index 1cc2ae9a..a9f0f065 100644
--- a/build/filesAfter.txt
+++ b/build/filesAfter.txt
@@ -15,4 +15,5 @@ bin\type-aliases.ps1
internal\scripts\taskEngine.ps1
internal\scripts\removalEvent.ps1
internal\scripts\variables.ps1
+internal\scripts\teppInputResources.ps1
internal\scripts\license.ps1
\ No newline at end of file
diff --git a/library/PSFramework/Message/LogHost.cs b/library/PSFramework/Message/LogHost.cs
index 24f331fd..18c8353a 100644
--- a/library/PSFramework/Message/LogHost.cs
+++ b/library/PSFramework/Message/LogHost.cs
@@ -70,6 +70,11 @@ public static class LogHost
/// Governs, whether a log of recent errors is kept in memory
///
public static bool ErrorLogEnabled = true;
+
+ ///
+ /// Whether the filesystem logging provider uses the modern logging style with CSV headers and extra columns
+ ///
+ public static bool FileSystemModernLog = false;
#endregion Defines
#region Queues
diff --git a/library/PSFramework/TabExpansion/TabExpansionHost.cs b/library/PSFramework/TabExpansion/TabExpansionHost.cs
index 3d2d2755..3ba524e8 100644
--- a/library/PSFramework/TabExpansion/TabExpansionHost.cs
+++ b/library/PSFramework/TabExpansion/TabExpansionHost.cs
@@ -21,5 +21,27 @@ public static class TabExpansionHost
///
public static Hashtable Cache = new Hashtable();
#endregion State information
+
+ #region Resources for individual tab completions
+ ///
+ /// Dictionary containing a list of hashtables to explicitly add properties when completing for specific output types.
+ /// Entries must have three properties:
+ /// - Name (Name of Property)
+ /// - Type (Type, not Typename, of the property. May be empty)
+ /// - TypeKnown (Boolean, whether the type is known)
+ /// Used by the Tab Completion: PSFramework-Input-ObjectProperty
+ ///
+ public static ConcurrentDictionary InputCompletionTypeData = new ConcurrentDictionary();
+
+ ///
+ /// Dictionary containing a list of hashtables to explicitly add properties when completing for specific commands
+ /// Entries must have three properties:
+ /// - Name (Name of Property)
+ /// - Type (Type, not Typename, of the property. May be empty)
+ /// - TypeKnown (Boolean, whether the type is known)
+ /// Used by the Tab Completion: PSFramework-Input-ObjectProperty
+ ///
+ public static ConcurrentDictionary InputCompletionCommandData = new ConcurrentDictionary();
+ #endregion Resources for individual tab completions
}
}