diff --git a/PSFramework/PSFramework.psd1 b/PSFramework/PSFramework.psd1
index 97f194b5..92b175b2 100644
--- a/PSFramework/PSFramework.psd1
+++ b/PSFramework/PSFramework.psd1
@@ -5,7 +5,7 @@
RootModule = 'PSFramework.psm1'
# Version number of this module.
- ModuleVersion = '0.10.30.165'
+ ModuleVersion = '0.10.31.176'
# ID used to uniquely identify this module
GUID = '8028b914-132b-431f-baa9-94a6952f21ff'
diff --git a/PSFramework/bin/PSFramework.dll b/PSFramework/bin/PSFramework.dll
index b6a19869..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 452d799b..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 09f7c0dc..a0750440 100644
--- a/PSFramework/bin/PSFramework.xml
+++ b/PSFramework/bin/PSFramework.xml
@@ -384,6 +384,17 @@
The message to write/log. The function name and timestamp will automatically be prepended.
+
+
+ A stored string to use to write the log.
+ Used in combination with the localization component.
+
+
+
+
+ Values to format into the localized string referred to in String.
+
+
Tags to add to the message written.
@@ -528,6 +539,11 @@
The input message with the error content included if desired
+
+
+ Unified representation of the various ways messages can be specified
+
+
The final message to use for internal logging
@@ -1223,11 +1239,28 @@
The language to display text with by default. Will use thread information if not set.
+
+
+ The default language to log in.
+
+
List of strings registered
+
+
+ Mapping module name to the language to use for logging.
+
+
+
+
+ Configure the module specific logging language.
+
+ The module to configure
+ The language to set. Leave empty to remove entry.
+
Writes a localized string. If needed creates it, then sets the text of the specified language.
@@ -1252,6 +1285,13 @@
The name of the string to request. Include the modulename
The localized string requested. Empty string if nothing.
+
+
+ Reads a localized string from the list of available strings for the purpose of logging
+
+ The name of the string to request. Include the modulename
+ The localized string requested. Empty string if nothing.
+
A string used for localized text
@@ -1277,6 +1317,11 @@
The actual string value of the text
+
+
+ The string value to use for logging purposes
+
+
Sets the text for a specific language
@@ -1734,11 +1779,17 @@
An individual entry for the message log
-
+
The message logged
+
+
+ The message to use for logging purposes.
+ Using the localized string feature, this allows maintaining uniform logging languages while still supporting localized
+
+
What kind of entry was this?
@@ -1809,12 +1860,22 @@
An error record associated with the message.
+
+
+ The string key to use when retrieving localized strings.
+
+
+
+
+ Values to format into the localized string
+
+
Creates an empty log entry
-
+
Creates a filled out log entry
@@ -1833,6 +1894,8 @@
The callstack that triggered the write.
The user responsible for running the code that is writing the message.
An associated error item.
+ The string key to use for retrieving localized strings
+ The values to format into the localized string
@@ -1929,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
@@ -1986,6 +2054,29 @@
An associated error record
The entry that is being written
+
+
+ Write a new entry to the log
+
+ The message to log
+ The type of the message logged
+ When was the message generated
+ What function wrote the message
+ What module did the function writing this message come from?
+ The tags that were applied to the message
+ At what level was the function written
+ The runspace the message is coming from
+ The computer the message was generated on
+ The file from which the message was written
+ The line on which the message was written
+ The object associated with a given message.
+ The callstack at the moment the message was written.
+ The name of the user under which the code being executed
+ The string key to use for retrieving localized strings
+ The values to format into the localized string
+ An associated error record
+ The entry that is being written
+
Condition and logic to be executed on message events
@@ -3488,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.
@@ -3567,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
@@ -3641,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.
@@ -3914,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
@@ -4593,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 78009a04..ec366c19 100644
--- a/PSFramework/changelog.md
+++ b/PSFramework/changelog.md
@@ -1,4 +1,17 @@
# CHANGELOG
+## 0.10.31.176 : 2019-01-13
+ - 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
+
## 0.10.30.165 : 2018-12-01
- New: Command Join-PSFPath performs multi-segment path joins and path normalization
- New: Command Remove-PSFAlias deletes global aliases
diff --git a/PSFramework/functions/flowcontrol/Stop-PSFFunction.ps1 b/PSFramework/functions/flowcontrol/Stop-PSFFunction.ps1
index 4812aaaf..13d1d192 100644
--- a/PSFramework/functions/flowcontrol/Stop-PSFFunction.ps1
+++ b/PSFramework/functions/flowcontrol/Stop-PSFFunction.ps1
@@ -1,129 +1,142 @@
function Stop-PSFFunction
{
- <#
- .SYNOPSIS
- Function that interrupts a function.
-
- .DESCRIPTION
- Function that interrupts a function.
-
- This function is a utility function used by other functions to reduce error catching overhead.
- It is designed to allow gracefully terminating a function with a warning by default and also allow opt-in into terminating errors.
- It also allows simple integration into loops.
-
- Note:
- When calling this function with the intent to terminate the calling function in non-ExceptionEnabled mode too, you need to add a return below the call.
-
- For a more detailed explanation - including commented full-scale implementation examples - see the associated help article:
- Get-Help about_psf_flowcontrol
-
- .PARAMETER Message
- A message to pass along, explaining just what the error was.
-
- .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 Category
- What category does this termination belong to?
- Is automatically set when passing an error record. Helps with differentiating exceptions without having to resort to text parsing.
-
- .PARAMETER ErrorRecord
- An option to include an inner exception in the error record (and in the exception thrown, if one is thrown).
- Use this, whenever you call Stop-PSFFunction in a catch block.
-
- Note:
- Pass the full error record, not just the exception.
-
- .PARAMETER Tag
- Tags to add to the message written.
- This allows filtering and grouping by category of message, targeting specific messages.
-
- .PARAMETER FunctionName
- The name of the function to crash.
- This parameter is very optional, since it automatically selects the name of the calling function.
- The function name is used as part of the errorid.
- That in turn allows easily figuring out, which exception belonged to which function when checking out the $error variable.
-
- .PARAMETER ModuleName
- The name of the module, the function to be crashed is part of.
- This parameter is very optional, since it automatically selects the name of the calling function.
-
- .PARAMETER File
- The file in which Stop-PSFFunction was called.
- Will be automatically set, but can be overridden when necessary.
+<#
+ .SYNOPSIS
+ Function that interrupts a function.
+
+ .DESCRIPTION
+ Function that interrupts a function.
+
+ This function is a utility function used by other functions to reduce error catching overhead.
+ It is designed to allow gracefully terminating a function with a warning by default and also allow opt-in into terminating errors.
+ It also allows simple integration into loops.
+
+ Note:
+ When calling this function with the intent to terminate the calling function in non-ExceptionEnabled mode too, you need to add a return below the call.
+
+ For a more detailed explanation - including commented full-scale implementation examples - see the associated help article:
+ Get-Help about_psf_flowcontrol
+
+ .PARAMETER Message
+ A message to pass along, explaining just what the error was.
+
+ .PARAMETER String
+ A stored string to use to write the log.
+ Used in combination with the localization component.
+ For more details see the help on Import-PSFLocalizedString and Get-PSFLocalizedString.
+
+ .PARAMETER StringValues
+ Values to format into the localized string referred to in the -String parameter.
+
+ .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 Category
+ What category does this termination belong to?
+ Is automatically set when passing an error record. Helps with differentiating exceptions without having to resort to text parsing.
+
+ .PARAMETER ErrorRecord
+ An option to include an inner exception in the error record (and in the exception thrown, if one is thrown).
+ Use this, whenever you call Stop-PSFFunction in a catch block.
+
+ Note:
+ Pass the full error record, not just the exception.
+
+ .PARAMETER Tag
+ Tags to add to the message written.
+ This allows filtering and grouping by category of message, targeting specific messages.
+
+ .PARAMETER FunctionName
+ The name of the function to crash.
+ This parameter is very optional, since it automatically selects the name of the calling function.
+ The function name is used as part of the errorid.
+ That in turn allows easily figuring out, which exception belonged to which function when checking out the $error variable.
+
+ .PARAMETER ModuleName
+ The name of the module, the function to be crashed is part of.
+ This parameter is very optional, since it automatically selects the name of the calling function.
+
+ .PARAMETER File
+ The file in which Stop-PSFFunction was called.
+ Will be automatically set, but can be overridden when necessary.
+
+ .PARAMETER Line
+ The line on which Stop-PSFFunction was called.
+ Will be automatically set, but can be overridden when necessary.
+
+ .PARAMETER Exception
+ Allows specifying an inner exception as input object. This will be passed on to the logging and used for messages.
+ When specifying both ErrorRecord AND Exception, Exception wins, but ErrorRecord is still used for record metadata.
+
+ .PARAMETER OverrideExceptionMessage
+ Disables automatic appending of exception messages.
+ Use in cases where you already have a speaking message interpretation and do not need the original message.
+
+ .PARAMETER Target
+ The object that was processed when the error was thrown.
+ For example, if you were trying to process a Database Server object when the processing failed, add the object here.
+ This object will be in the error record (which will be written, even in non-silent mode, just won't show it).
+ If you specify such an object, it becomes simple to actually figure out, just where things failed at.
+
+ .PARAMETER Continue
+ This will cause the function to call continue while not running with exceptions enabled (-EnableException).
+ Useful when mass-processing items where an error shouldn't break the loop.
+
+ .PARAMETER SilentlyContinue
+ This will cause the function to call continue while running with exceptions enabled (-EnableException).
+ Useful when mass-processing items where an error shouldn't break the loop.
+
+ .PARAMETER ContinueLabel
+ When specifying a label in combination with "-Continue" or "-SilentlyContinue", this function will call continue with this specified label.
+ Helpful when trying to continue on an upper level named loop.
+
+ .PARAMETER Cmdlet
+ The $PSCmdlet object of the calling command.
+ Used to write exceptions in a more hidden manner, avoiding exposing internal script text in the default message display.
+
+ .PARAMETER StepsUpward
+ When not throwing an exception and not calling continue, Stop-PSFFunction signals the calling command to stop.
+ In some cases you may want to signal a step or more further up the chain (notably from helper functions within a function).
+ This parameter allows you to add additional steps up the callstack that it will notify.
+
+ .EXAMPLE
+ Stop-PSFFunction -Message "Foo failed bar!" -EnableException $EnableException -ErrorRecord $_
+ return
+
+ Depending on whether $EnableException is true or false it will:
+ - Throw a bloody terminating error. Game over.
+ - Write a nice warning about how Foo failed bar, then terminate the function. The return on the next line will then end the calling function.
+
+ .EXAMPLE
+ Stop-PSFFunction -Message "Foo failed bar!" -EnableException $EnableException -Category InvalidOperation -Target $foo -Continue
- .PARAMETER Line
- The line on which Stop-PSFFunction was called.
- Will be automatically set, but can be overridden when necessary.
-
- .PARAMETER Target
- The object that was processed when the error was thrown.
- For example, if you were trying to process a Database Server object when the processing failed, add the object here.
- This object will be in the error record (which will be written, even in non-silent mode, just won't show it).
- If you specify such an object, it becomes simple to actually figure out, just where things failed at.
-
- .PARAMETER Continue
- This will cause the function to call continue while not running with exceptions enabled (-EnableException).
- Useful when mass-processing items where an error shouldn't break the loop.
-
- .PARAMETER SilentlyContinue
- This will cause the function to call continue while running with exceptions enabled (-EnableException).
- Useful when mass-processing items where an error shouldn't break the loop.
-
- .PARAMETER ContinueLabel
- When specifying a label in combination with "-Continue" or "-SilentlyContinue", this function will call continue with this specified label.
- Helpful when trying to continue on an upper level named loop.
-
- .PARAMETER Exception
- Allows specifying an inner exception as input object. This will be passed on to the logging and used for messages.
- When specifying both ErrorRecord AND Exception, Exception wins, but ErrorRecord is still used for record metadata.
-
- .PARAMETER OverrideExceptionMessage
- Disables automatic appending of exception messages.
- Use in cases where you already have a speaking message interpretation and do not need the original message.
-
- .PARAMETER Cmdlet
- The $PSCmdlet object of the calling command.
- Used to write exceptions in a more hidden manner, avoiding exposing internal script text in the default message display.
-
- .PARAMETER StepsUpward
- When not throwing an exception and not calling continue, Stop-PSFFunction signals the calling command to stop.
- In some cases you may want to signal a step or more further up the chain (notably from helper functions within a function).
- This parameter allows you to add additional steps up the callstack that it will notify.
-
- .EXAMPLE
- Stop-PSFFunction -Message "Foo failed bar!" -EnableException $EnableException -ErrorRecord $_
- return
-
- Depending on whether $EnableException is true or false it will:
- - Throw a bloody terminating error. Game over.
- - Write a nice warning about how Foo failed bar, then terminate the function. The return on the next line will then end the calling function.
-
- .EXAMPLE
- Stop-PSFFunction -Message "Foo failed bar!" -EnableException $EnableException -Category InvalidOperation -Target $foo -Continue
-
- Depending on whether $EnableException is true or false it will:
- - Throw a bloody terminating error. Game over.
- - Write a nice warning about how Foo failed bar, then call continue to process the next item in the loop.
- In both cases, the error record added to $error will have the content of $foo added, the better to figure out what went wrong.
- #>
+ Depending on whether $EnableException is true or false it will:
+ - Throw a bloody terminating error. Game over.
+ - Write a nice warning about how Foo failed bar, then call continue to process the next item in the loop.
+ In both cases, the error record added to $error will have the content of $foo added, the better to figure out what went wrong.
+#>
[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
- [CmdletBinding(DefaultParameterSetName = 'Plain', HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Stop-PSFFunction')]
+ [CmdletBinding(DefaultParameterSetName = 'Message', HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Stop-PSFFunction')]
param (
- [Parameter(Mandatory = $true)]
+ [Parameter(Mandatory = $true, ParameterSetName = 'Message')]
[string]
$Message,
+ [Parameter(Mandatory = $true, ParameterSetName = 'String')]
+ [string]
+ $String,
+
+ [Parameter(ParameterSetName = 'String')]
+ [object[]]
+ $StringValues,
+
[bool]
$EnableException,
- [Parameter(ParameterSetName = 'Plain')]
- [Parameter(ParameterSetName = 'Exception')]
[System.Management.Automation.ErrorCategory]
$Category = ([System.Management.Automation.ErrorCategory]::NotSpecified),
- [Parameter(ParameterSetName = 'Exception')]
[Alias('InnerErrorRecord')]
[System.Management.Automation.ErrorRecord[]]
$ErrorRecord,
@@ -214,6 +227,25 @@
#region Message Handling
$records = @()
+ $paramWritePSFMessage = @{
+ Level = 'Warning'
+ EnableException = $EnableException
+ FunctionName = $FunctionName
+ Target = $Target
+ Tag = $Tag
+ ModuleName = $ModuleName
+ OverrideExceptionMessage = $true
+ File = $File
+ Line = $Line
+ }
+ if ($Message) { $paramWritePSFMessage["Message"] = $Message }
+ else
+ {
+ $paramWritePSFMessage["String"] = $String
+ $paramWritePSFMessage["StringValues"] = $StringValues
+ }
+
+
if ($ErrorRecord -or $Exception)
{
if ($ErrorRecord)
@@ -232,8 +264,8 @@
}
# Manage Debugging
- if ($EnableException) { Write-PSFMessage -Level Warning -Message $Message -EnableException $EnableException -FunctionName $FunctionName -Target $Target -ErrorRecord $records -Tag $Tag -ModuleName $ModuleName -OverrideExceptionMessage:$OverrideExceptionMessage -File $File -Line $Line 3>$null }
- else { Write-PSFMessage -Level Warning -Message $Message -EnableException $EnableException -FunctionName $FunctionName -Target $Target -ErrorRecord $records -Tag $Tag -ModuleName $ModuleName -OverrideExceptionMessage:$OverrideExceptionMessage -File $File -Line $Line }
+ if ($EnableException) { Write-PSFMessage -ErrorRecord $records @paramWritePSFMessage 3>$null }
+ else { Write-PSFMessage -ErrorRecord $records @paramWritePSFMessage }
}
else
{
@@ -241,8 +273,8 @@
$records += New-Object System.Management.Automation.ErrorRecord($Exception, "$($ModuleName)_$FunctionName", $Category, $Target)
# Manage Debugging
- if ($EnableException) { Write-PSFMessage -Level Warning -Message $Message -EnableException $EnableException -FunctionName $FunctionName -Target $Target -ErrorRecord $records -Tag $Tag -ModuleName $ModuleName -OverrideExceptionMessage:$true -File $File -Line $Line 3>$null }
- else { Write-PSFMessage -Level Warning -Message $Message -EnableException $EnableException -FunctionName $FunctionName -Target $Target -ErrorRecord $records -Tag $Tag -ModuleName $ModuleName -OverrideExceptionMessage:$true -File $File -Line $Line }
+ if ($EnableException) { Write-PSFMessage -ErrorRecord $records @paramWritePSFMessage 3>$null }
+ else { Write-PSFMessage -ErrorRecord $records @paramWritePSFMessage }
}
#endregion Message Handling
diff --git a/PSFramework/functions/localization/Import-PSFLocalizedString.ps1 b/PSFramework/functions/localization/Import-PSFLocalizedString.ps1
index f9933ee6..5c5309ed 100644
--- a/PSFramework/functions/localization/Import-PSFLocalizedString.ps1
+++ b/PSFramework/functions/localization/Import-PSFLocalizedString.ps1
@@ -43,15 +43,18 @@
begin
{
- try { $resolvedPath = Resolve-PSFPath -Path $Path -Provider FileSystem -SingleItem }
+ try { $resolvedPath = Resolve-PSFPath -Path $Path -Provider FileSystem }
catch { Stop-PSFFunction -Message "Failed to resolve path: $Path" -EnableException $true -Cmdlet $PSCmdlet -ErrorRecord $_ }
}
process
{
- $data = Import-PowerShellDataFile -Path $resolvedPath
- foreach ($key in $data.Keys)
+ foreach ($pathItem in $resolvedPath)
{
- [PSFramework.Localization.LocalizationHost]::Write($Module, $key, $Language, $data[$key])
+ $data = Import-PowerShellDataFile -Path $pathItem
+ foreach ($key in $data.Keys)
+ {
+ [PSFramework.Localization.LocalizationHost]::Write($Module, $key, $Language, $data[$key])
+ }
}
}
}
\ No newline at end of file
diff --git a/PSFramework/internal/configurations/general.ps1 b/PSFramework/internal/configurations/general.ps1
index 7fdb57b7..72553ae2 100644
--- a/PSFramework/internal/configurations/general.ps1
+++ b/PSFramework/internal/configurations/general.ps1
@@ -9,4 +9,5 @@ Set-PSFConfig -Module PSFramework -Name 'Text.Encoding.DefaultWrite' -Value 'utf
Set-PSFConfig -Module PSFramework -Name 'Text.Encoding.DefaultRead' -Value 'utf-8' -Initialize -Validation 'string' -Description 'The default encoding to use when reading from file. Only applied by implementing commands.'
# Localization Stuff
-Set-PSFConfig -Module PSFramework -Name 'Localization.Language' -Value ([System.Globalization.CultureInfo]::CurrentUICulture.Name) -Initialize -Handler { [PSFramework.Localization.LocalizationHost]::Language = $args[0] } -Validation 'languagecode' -Description 'The language the current PowerShell session is operating under'
\ No newline at end of file
+Set-PSFConfig -Module PSFramework -Name 'Localization.Language' -Value ([System.Globalization.CultureInfo]::CurrentUICulture.Name) -Initialize -Handler { [PSFramework.Localization.LocalizationHost]::Language = $args[0] } -Validation 'languagecode' -Description 'The language the current PowerShell session is operating under'
+Set-PSFConfig -Module PSFramework -Name 'Localization.LoggingLanguage' -Value 'en-US' -Initialize -Handler { [PSFramework.Localization.LocalizationHost]::LoggingLanguage = $args[0] } -Validation 'languagecode' -Description 'The language the current PowerShell session is operating under'
\ No newline at end of file
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/configurationvalidation/credential.ps1 b/PSFramework/internal/configurationvalidation/credential.ps1
new file mode 100644
index 00000000..ef8131e3
--- /dev/null
+++ b/PSFramework/internal/configurationvalidation/credential.ps1
@@ -0,0 +1,30 @@
+Register-PSFConfigValidation -Name "credential" -ScriptBlock {
+ param (
+ $Value
+ )
+
+ $Result = New-Object PSObject -Property @{
+ Success = $True
+ Value = $null
+ Message = ""
+ }
+ try
+ {
+ if ($Value.GetType().FullName -ne "System.Management.Automation.PSCredential")
+ {
+ $Result.Message = "Not a credential: $Value"
+ $Result.Success = $False
+ return $Result
+ }
+ }
+ catch
+ {
+ $Result.Message = "Not a credential: $Value"
+ $Result.Success = $False
+ return $Result
+ }
+
+ $Result.Value = $Value
+
+ return $Result
+}
\ No newline at end of file
diff --git a/PSFramework/internal/functions/localization/Import-LocalizedString.ps1 b/PSFramework/internal/functions/localization/Import-LocalizedString.ps1
index d24075e2..1910cbde 100644
--- a/PSFramework/internal/functions/localization/Import-LocalizedString.ps1
+++ b/PSFramework/internal/functions/localization/Import-LocalizedString.ps1
@@ -42,15 +42,18 @@
begin
{
- try { $resolvedPath = Resolve-PSFPath -Path $Path -Provider FileSystem -SingleItem }
+ try { $resolvedPath = Resolve-PSFPath -Path $Path -Provider FileSystem }
catch { Stop-PSFFunction -Message "Failed to resolve path: $Path" -EnableException $true -Cmdlet $PSCmdlet -ErrorRecord $_ }
}
process
{
- $data = Import-PowerShellDataFile -Path $resolvedPath
- foreach ($key in $data.Keys)
+ foreach ($pathItem in $resolvedPath)
{
- [PSFramework.Localization.LocalizationHost]::Write($Module, $key, $Language, $data[$key])
+ $data = Import-PowerShellDataFile -Path $pathItem
+ foreach ($key in $data.Keys)
+ {
+ [PSFramework.Localization.LocalizationHost]::Write($Module, $key, $Language, $data[$key])
+ }
}
}
}
\ No newline at end of file
diff --git a/PSFramework/internal/loggingProviders/filesystem.provider.ps1 b/PSFramework/internal/loggingProviders/filesystem.provider.ps1
index b215a6da..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-Object ComputerName, Timestamp, Level, 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/loggingProviders/gelf.provider.ps1 b/PSFramework/internal/loggingProviders/gelf.provider.ps1
index b9b8a9de..369eef46 100644
--- a/PSFramework/internal/loggingProviders/gelf.provider.ps1
+++ b/PSFramework/internal/loggingProviders/gelf.provider.ps1
@@ -31,7 +31,7 @@ $message_Event = {
)
$gelf_params = $gelf_paramSendPsgelfTcp.Clone()
- $gelf_params['ShortMessage'] = $Message.Message
+ $gelf_params['ShortMessage'] = $Message.LogMessage
$gelf_params['HostName'] = $Message.ComputerName
$gelf_params['DateTime'] = $Message.Timestamp
@@ -55,7 +55,7 @@ $message_Event = {
# build the additional fields
$gelf_properties = $Message.PSObject.Properties | Where-Object {
- $_.Name -notin @('Message', 'ComputerName', 'Timestamp', 'Level', 'ErrorRecord')
+ $_.Name -notin @('Message', 'LogMessage', 'ComputerName', 'Timestamp', 'Level', 'ErrorRecord')
}
$gelf_params['AdditionalField'] = @{}
diff --git a/PSFramework/internal/loggingProviders/logfile.provider.ps1 b/PSFramework/internal/loggingProviders/logfile.provider.ps1
index 66ecc976..5b7ff1b5 100644
--- a/PSFramework/internal/loggingProviders/logfile.provider.ps1
+++ b/PSFramework/internal/loggingProviders/logfile.provider.ps1
@@ -120,13 +120,23 @@ $begin_event = {
if ($logfile_headers -contains 'Tags')
{
$logfile_headers = $logfile_headers | ForEach-Object {
- if ($_ -ne 'Tags') { $_ }
- else
+ switch ($_)
{
- @{
- Name = 'Tags'
- Expression = { $_.Tags -join "," }
+ 'Tags'
+ {
+ @{
+ Name = 'Tags'
+ Expression = { $_.Tags -join "," }
+ }
+ }
+ 'Message'
+ {
+ @{
+ Name = 'Message'
+ Expression = { $_.LogMessage }
+ }
}
+ default { $_ }
}
}
}
diff --git a/PSFramework/internal/scripts/environment.ps1 b/PSFramework/internal/scripts/environment.ps1
index ffaa24e0..796380b9 100644
--- a/PSFramework/internal/scripts/environment.ps1
+++ b/PSFramework/internal/scripts/environment.ps1
@@ -87,4 +87,7 @@ if (($PSVersionTable.PSVersion.Major -ge 6) -and ($PSVersionTable.OS -notlike "*
if (-not ([PSFramework.Message.LogHost]::LoggingPath)) { [PSFramework.Message.LogHost]::LoggingPath = $script:path_Logging }
-[PSFramework.PSFCore.PSFCoreHost]::ModuleRoot = $script:ModuleRoot
\ No newline at end of file
+[PSFramework.PSFCore.PSFCoreHost]::ModuleRoot = $script:ModuleRoot
+# Run the library initialization logic
+# Needed before the configuration system loads
+[PSFramework.PSFCore.PSFCoreHost]::Initialize()
\ No newline at end of file
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/removalEvent.ps1 b/PSFramework/internal/scripts/removalEvent.ps1
index 7b0f57c0..8abb4b8f 100644
--- a/PSFramework/internal/scripts/removalEvent.ps1
+++ b/PSFramework/internal/scripts/removalEvent.ps1
@@ -4,6 +4,7 @@ $PSF_OnRemoveScript = {
if ([runspace]::DefaultRunspace.Id -eq 1)
{
Get-PSFRunspace | Stop-PSFRunspace
+ [PSFramework.PSFCore.PSFCoreHost]::Uninitialize()
}
# Properly disconnect all remote sessions still held open
diff --git a/PSFramework/internal/scripts/strings.ps1 b/PSFramework/internal/scripts/strings.ps1
index 36869f41..8ac830f6 100644
--- a/PSFramework/internal/scripts/strings.ps1
+++ b/PSFramework/internal/scripts/strings.ps1
@@ -1,4 +1,3 @@
-Import-LocalizedString -Path (Resolve-Path "$script:ModuleRoot\en-us\stringsAssembly.psd1") -Module PSFramework -Language 'en-US'
-
+Import-LocalizedString -Path "$script:ModuleRoot\en-us\*.psd1" -Module PSFramework -Language 'en-US'
$script:strings = Get-PSFLocalizedString -Module PSFramework
\ 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
new file mode 100644
index 00000000..55acbefa
--- /dev/null
+++ b/PSFramework/internal/tepp/scripts/input.ps1
@@ -0,0 +1,710 @@
+Register-PSFTeppScriptblock -Name PSFramework-Input-ObjectProperty -ScriptBlock {
+ #region Utility Functions
+ function Get-Property
+ {
+ [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 @{ } }
+ }
+ }
+
+ function Update-Property
+ {
+ [CmdletBinding()]
+ param (
+ [Hashtable]
+ $Property,
+
+ $Step
+ )
+
+ $properties = @{ }
+ #region Expand Property
+ if ($Step.ExpandProperty)
+ {
+ if (-not ($Property[$Step.ExpandProperty])) { return $properties }
+
+ $expanded = $Property[$Step.ExpandProperty]
+ if (-not $expanded.TypeKnown) { return $properties }
+
+ foreach ($member in $expanded.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[$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'
+ {
+ if ($stepProperty.Filter) { continue main }
+ if ($properties[$stepProperty.Name]) { continue main }
+
+ foreach ($exclusion in $Step.Excluded)
+ {
+ 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)
+ {
+ switch ($position)
+ {
+ 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 "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)
+ {
+ 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
+ }
+ 'System.Management.Automation.Language.ConstantExpressionAst'
+ {
+ if (-not $parameterName)
+ {
+ switch ($position)
+ {
+ 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 "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)
+ {
+ 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 = ''
+ }
+ }
+ $index = $index + 1
+ }
+ }
+ #endregion Process Ast
+
+ #region Convert Results
+ $resultsProcessed = [pscustomobject]@{
+ HasIncludeFilter = $false
+ RawResult = $results
+ Properties = @()
+ Excluded = $results.ExcludeProperty
+ ExpandProperty = $results.ExpandProperty
+ KeepInputObject = $results.KeepInputObject
+ }
+
+ switch ($CommandName)
+ {
+ #region Select-Object
+ 'Select-Object'
+ {
+ #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
+ {
+ 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)
+ {
+ $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
+ }
+ }
+ }
+ }
+ #endregion Alias Properties
+ }
+ #endregion Select-PSFObject
+ }
+ #endregion Convert Results
+
+ $resultsProcessed
+ }
+ #endregion Utility Functions
+
+ # 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 }
+
+ $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)
+ {
+ # 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)
+ {
+ '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/scripts/tepp-scriptblockname.ps1 b/PSFramework/internal/tepp/scripts/tepp-scriptblockname.ps1
deleted file mode 100644
index 256c99bd..00000000
--- a/PSFramework/internal/tepp/scripts/tepp-scriptblockname.ps1
+++ /dev/null
@@ -1,3 +0,0 @@
-Register-PSFTeppScriptblock -Name 'PSFramework-tepp-scriptblockname' -ScriptBlock {
- [PSFramework.TabExpansion.TabExpansionHost]::Scripts.Keys
-}
\ No newline at end of file
diff --git a/PSFramework/internal/tepp/scripts/tepp.ps1 b/PSFramework/internal/tepp/scripts/tepp.ps1
new file mode 100644
index 00000000..ed7f3a5a
--- /dev/null
+++ b/PSFramework/internal/tepp/scripts/tepp.ps1
@@ -0,0 +1,18 @@
+Register-PSFTeppScriptblock -Name 'PSFramework-tepp-scriptblockname' -ScriptBlock {
+ [PSFramework.TabExpansion.TabExpansionHost]::Scripts.Keys
+}
+
+Register-PSFTeppScriptblock -Name 'PSFramework-tepp-parametername' -ScriptBlock {
+ if ($fakeBoundParameter.Command)
+ {
+ $common = 'Verbose', 'Debug', 'ErrorAction', 'WarningAction', 'InformationAction', 'ErrorVariable', 'WarningVariable', 'InformationVariable', 'OutVariable', 'OutBuffer', 'PipelineVariable', 'WhatIf', 'Confirm'
+
+ try
+ {
+ $command = Get-Command $fakeBoundParameter.Command
+ if ($command -is [System.Management.Automation.AliasInfo]) { $command = $command.ResolvedCommand }
+ $command.Parameters.Keys | Where-Object { $_ -notin $common }
+ }
+ catch { }
+ }
+}
\ No newline at end of file
diff --git a/PSFramework/internal/tepp/tepp-assignment.ps1 b/PSFramework/internal/tepp/tepp-assignment.ps1
index 8db482a9..ba350bde 100644
--- a/PSFramework/internal/tepp/tepp-assignment.ps1
+++ b/PSFramework/internal/tepp/tepp-assignment.ps1
@@ -28,6 +28,7 @@ Register-PSFTeppArgumentCompleter -Command Get-PSFLicense -Parameter Filter -Nam
#region Localization
Register-PSFTeppArgumentCompleter -Command Import-PSFLocalizedString -Parameter Language -Name 'PSFramework-LanguageNames'
+Register-PSFTeppArgumentCompleter -Command Get-PSFLocalizedString -Parameter Module -Name 'PSFramework-LocalizedStrings-Modules'
Register-PSFTeppArgumentCompleter -Command Get-PSFLocalizedString -Parameter Name -Name 'PSFramework-LocalizedStrings-Names'
#endregion Localization
@@ -62,8 +63,16 @@ Register-PSFTeppArgumentCompleter -Command Import-PSFClixml -Parameter Encoding
#region Tab Completion
Register-PSFTeppArgumentCompleter -Command Set-PSFTeppResult -Parameter TabCompletion -Name 'PSFramework-tepp-scriptblockname'
+Register-PSFTeppArgumentCompleter -Command Register-PSFTeppArgumentCompleter -Parameter Name -Name 'PSFramework-tepp-scriptblockname'
+Register-PSFTeppArgumentCompleter -Command Register-PSFTeppArgumentCompleter -Parameter Parameter -Name 'PSFramework-tepp-parametername'
#endregion Tab Completion
#region Utility
+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/Commands/TestPSFShouldProcessCommand.cs b/library/PSFramework/Commands/TestPSFShouldProcessCommand.cs
index 4640b93c..c3c30ef5 100644
--- a/library/PSFramework/Commands/TestPSFShouldProcessCommand.cs
+++ b/library/PSFramework/Commands/TestPSFShouldProcessCommand.cs
@@ -5,7 +5,7 @@ namespace PSFramework.Commands
///
/// Implements the Test-PSFShouldProcess command
///
- [Cmdlet("Test", "PSFShouldProcess")]
+ [Cmdlet("Test", "PSFShouldProcess", SupportsShouldProcess = true)]
public class TestPSFShouldProcessCommand : Cmdlet
{
///
diff --git a/library/PSFramework/Commands/WritePSFMessageCommand.cs b/library/PSFramework/Commands/WritePSFMessageCommand.cs
index 4df6e9c0..47cb3c13 100644
--- a/library/PSFramework/Commands/WritePSFMessageCommand.cs
+++ b/library/PSFramework/Commands/WritePSFMessageCommand.cs
@@ -10,7 +10,7 @@ namespace PSFramework.Commands
///
/// Cmdlet performing message handling and logging
///
- [Cmdlet("Write","PSFMessage")]
+ [Cmdlet("Write","PSFMessage", DefaultParameterSetName = "Message")]
public class WritePSFMessageCommand : PSCmdlet
{
#region Parameters
@@ -34,11 +34,24 @@ public class WritePSFMessageCommand : PSCmdlet
///
/// The message to write/log. The function name and timestamp will automatically be prepended.
///
- [Parameter(Mandatory = true, Position = 0)]
+ [Parameter(Mandatory = true, Position = 0, ParameterSetName = "Message")]
[AllowEmptyString]
[AllowNull]
public string Message;
+ ///
+ /// A stored string to use to write the log.
+ /// Used in combination with the localization component.
+ ///
+ [Parameter(Mandatory = true, ParameterSetName = "String")]
+ public string String;
+
+ ///
+ /// Values to format into the localized string referred to in String.
+ ///
+ [Parameter(ParameterSetName = "String")]
+ public object[] StringValues;
+
///
/// Tags to add to the message written.
/// This allows filtering and grouping by category of message, targeting specific messages.
@@ -207,18 +220,31 @@ private string _errorQualifiedMessage
get
{
if (ErrorRecord == null)
- return Message;
+ return _ResolvedMessage;
if (ErrorRecord.Length == 0)
- return Message;
+ return _ResolvedMessage;
if (OverrideExceptionMessage.ToBool())
- return Message;
+ return _ResolvedMessage;
if (Regex.IsMatch(Message, Regex.Escape(ErrorRecord[0].Exception.Message)))
- return Message;
+ return _ResolvedMessage;
+
+ return String.Format("{0} | {1}", _ResolvedMessage, ErrorRecord[0].Exception.Message);
+ }
+ }
- return String.Format("{0} | {1}", Message, ErrorRecord[0].Exception.Message);
+ ///
+ /// Unified representation of the various ways messages can be specified
+ ///
+ private string _ResolvedMessage
+ {
+ get
+ {
+ if (!String.IsNullOrEmpty(String))
+ return String.Format(Localization.LocalizationHost.Read(String.Format("{0}.{1}", ModuleName, String)), StringValues);
+ return Message;
}
}
@@ -516,7 +542,7 @@ whether this function was called from Stop-PSFFunction.
#endregion Message handling
#region Logging
- LogEntry entry = LogHost.WriteLogEntry(_MessageSystem, channels, _timestamp, FunctionName, ModuleName, _Tags, Level, System.Management.Automation.Runspaces.Runspace.DefaultRunspace.InstanceId, Environment.MachineName, File, Line, _callStack, String.Format("{0}\\{1}",Environment.UserDomainName, Environment.UserName), errorRecord, Target);
+ LogEntry entry = LogHost.WriteLogEntry(_MessageSystem, channels, _timestamp, FunctionName, ModuleName, _Tags, Level, System.Management.Automation.Runspaces.Runspace.DefaultRunspace.InstanceId, Environment.MachineName, File, Line, _callStack, String.Format("{0}\\{1}",Environment.UserDomainName, Environment.UserName), errorRecord, String, StringValues, Target);
#endregion Logging
foreach (MessageEventSubscription subscription in MessageHost.Events.Values)
diff --git a/library/PSFramework/Localization/LocalString.cs b/library/PSFramework/Localization/LocalString.cs
index 89a46530..a70d97bb 100644
--- a/library/PSFramework/Localization/LocalString.cs
+++ b/library/PSFramework/Localization/LocalString.cs
@@ -51,6 +51,31 @@ public string Value
}
}
+ ///
+ /// The string value to use for logging purposes
+ ///
+ public string LogValue
+ {
+ get
+ {
+ if (_Strings.Keys.Count == 0)
+ throw new InvalidOperationException("Cannot offer string value without at least ONE string registered");
+
+ if (LocalizationHost.ModuleLoggingLanguage.ContainsKey(Module) && _Strings.ContainsKey(LocalizationHost.ModuleLoggingLanguage[Module]))
+ return _Strings[LocalizationHost.ModuleLoggingLanguage[Module]];
+ if (_Strings.ContainsKey(LocalizationHost.LoggingLanguage))
+ return _Strings[LocalizationHost.LoggingLanguage];
+ if (_Strings.ContainsKey(LocalizationHost.Language))
+ return _Strings[LocalizationHost.Language];
+ if (_Strings.ContainsKey(CultureInfo.CurrentUICulture.Name))
+ return _Strings[CultureInfo.CurrentUICulture.Name];
+ if (_Strings.ContainsKey("en-US"))
+ return _Strings["en-US"];
+
+ return _Strings.Values.First();
+ }
+ }
+
///
/// Sets the text for a specific language
///
diff --git a/library/PSFramework/Localization/LocalizationHost.cs b/library/PSFramework/Localization/LocalizationHost.cs
index 24a889e2..efc3b3ff 100644
--- a/library/PSFramework/Localization/LocalizationHost.cs
+++ b/library/PSFramework/Localization/LocalizationHost.cs
@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
namespace PSFramework.Localization
{
@@ -12,10 +13,32 @@ public static class LocalizationHost
///
public static string Language = "";
+ ///
+ /// The default language to log in.
+ ///
+ public static string LoggingLanguage = "en-US";
+
///
/// List of strings registered
///
- public static Dictionary Strings = new Dictionary();
+ public static Dictionary Strings = new Dictionary(StringComparer.InvariantCultureIgnoreCase);
+
+ ///
+ /// Mapping module name to the language to use for logging.
+ ///
+ public static Dictionary ModuleLoggingLanguage = new Dictionary(StringComparer.InvariantCultureIgnoreCase);
+
+ ///
+ /// Configure the module specific logging language.
+ ///
+ /// The module to configure
+ /// The language to set. Leave empty to remove entry.
+ public static void SetLoggingLanguage(string Module, string Language)
+ {
+ ModuleLoggingLanguage[Module] = Language;
+ if (String.IsNullOrEmpty(Language))
+ ModuleLoggingLanguage.Remove(Module);
+ }
///
/// Writes a localized string. If needed creates it, then sets the text of the specified language.
@@ -56,5 +79,17 @@ public static string Read(string FullName)
return "";
return Strings[FullName].Value;
}
+
+ ///
+ /// Reads a localized string from the list of available strings for the purpose of logging
+ ///
+ /// The name of the string to request. Include the modulename
+ /// The localized string requested. Empty string if nothing.
+ public static string ReadLog(string FullName)
+ {
+ if (!Strings.ContainsKey(FullName))
+ return String.Format("", FullName);
+ return Strings[FullName].LogValue;
+ }
}
}
diff --git a/library/PSFramework/Message/LogEntry.cs b/library/PSFramework/Message/LogEntry.cs
index b0f7bd0f..11c57082 100644
--- a/library/PSFramework/Message/LogEntry.cs
+++ b/library/PSFramework/Message/LogEntry.cs
@@ -1,4 +1,5 @@
-using System;
+using PSFramework.Localization;
+using System;
using System.Collections.Generic;
using System.Management.Automation;
@@ -13,7 +14,36 @@ public class LogEntry
///
/// The message logged
///
- public string Message;
+ public string Message
+ {
+ get
+ {
+ if (String.IsNullOrEmpty(String))
+ return _Message;
+ if (null == StringValue)
+ return LocalizationHost.Read(String.Format("{0}.{1}", ModuleName, String));
+ return String.Format(LocalizationHost.Read(String.Format("{0}.{1}", ModuleName, String)), StringValue);
+ }
+ set { _Message = value; }
+ }
+ private string _Message;
+
+ ///
+ /// The message to use for logging purposes.
+ /// Using the localized string feature, this allows maintaining uniform logging languages while still supporting localized
+ ///
+ public string LogMessage
+ {
+ get
+ {
+ if (String.IsNullOrEmpty(String))
+ return _Message;
+ if (null == StringValue)
+ return LocalizationHost.Read(String.Format("{0}.{1}", ModuleName, String));
+ return String.Format(LocalizationHost.ReadLog(String.Format("{0}.{1}", ModuleName, String)), StringValue);
+ }
+ set { }
+ }
///
/// What kind of entry was this?
@@ -85,6 +115,16 @@ public class LogEntry
///
public PsfExceptionRecord ErrorRecord;
+ ///
+ /// The string key to use when retrieving localized strings.
+ ///
+ public string String;
+
+ ///
+ /// Values to format into the localized string
+ ///
+ public object[] StringValue;
+
///
/// Creates an empty log entry
///
@@ -111,7 +151,9 @@ public LogEntry()
/// The callstack that triggered the write.
/// The user responsible for running the code that is writing the message.
/// An associated error item.
- public LogEntry(string Message, LogEntryType Type, DateTime Timestamp, string FunctionName, string ModuleName, List Tags, MessageLevel Level, Guid Runspace, string ComputerName, object TargetObject, string File, int Line, CallStack CallStack, string Username, PsfExceptionRecord ErrorRecord)
+ /// The string key to use for retrieving localized strings
+ /// The values to format into the localized string
+ public LogEntry(string Message, LogEntryType Type, DateTime Timestamp, string FunctionName, string ModuleName, List Tags, MessageLevel Level, Guid Runspace, string ComputerName, object TargetObject, string File, int Line, CallStack CallStack, string Username, PsfExceptionRecord ErrorRecord, string String, object[] StringValue)
{
this.Message = Message;
this.Type = Type;
@@ -128,6 +170,8 @@ public LogEntry(string Message, LogEntryType Type, DateTime Timestamp, string Fu
this.CallStack = CallStack;
this.Username = Username;
this.ErrorRecord = ErrorRecord;
+ this.String = String;
+ this.StringValue = StringValue;
}
}
}
\ No newline at end of file
diff --git a/library/PSFramework/Message/LogHost.cs b/library/PSFramework/Message/LogHost.cs
index ec11f493..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
@@ -163,7 +168,33 @@ public static PsfExceptionRecord WriteErrorEntry(ErrorRecord[] Record, string Fu
/// The entry that is being written
public static LogEntry WriteLogEntry(string Message, LogEntryType Type, DateTime Timestamp, string FunctionName, string ModuleName, List Tags, MessageLevel Level, Guid Runspace, string ComputerName, string File, int Line, IEnumerable CallStack, string Username, PsfExceptionRecord ErrorRecord, object TargetObject = null)
{
- LogEntry temp = new LogEntry(Message, Type, Timestamp, FunctionName, ModuleName, Tags, Level, Runspace, ComputerName, TargetObject, File, Line, new PSFramework.Message.CallStack(CallStack), Username, ErrorRecord);
+ return WriteLogEntry(Message, Type, Timestamp, FunctionName, ModuleName, Tags, Level, Runspace, ComputerName, File, Line, CallStack, Username, ErrorRecord, "", null, TargetObject);
+ }
+
+ ///
+ /// Write a new entry to the log
+ ///
+ /// The message to log
+ /// The type of the message logged
+ /// When was the message generated
+ /// What function wrote the message
+ /// What module did the function writing this message come from?
+ /// The tags that were applied to the message
+ /// At what level was the function written
+ /// The runspace the message is coming from
+ /// The computer the message was generated on
+ /// The file from which the message was written
+ /// The line on which the message was written
+ /// The object associated with a given message.
+ /// The callstack at the moment the message was written.
+ /// The name of the user under which the code being executed
+ /// The string key to use for retrieving localized strings
+ /// The values to format into the localized string
+ /// An associated error record
+ /// The entry that is being written
+ public static LogEntry WriteLogEntry(string Message, LogEntryType Type, DateTime Timestamp, string FunctionName, string ModuleName, List Tags, MessageLevel Level, Guid Runspace, string ComputerName, string File, int Line, IEnumerable CallStack, string Username, PsfExceptionRecord ErrorRecord, string String, object[] StringValue, object TargetObject = null)
+ {
+ LogEntry temp = new LogEntry(Message, Type, Timestamp, FunctionName, ModuleName, Tags, Level, Runspace, ComputerName, TargetObject, File, Line, new PSFramework.Message.CallStack(CallStack), Username, ErrorRecord, String, StringValue);
if (MessageLogFileEnabled) { OutQueueLog.Enqueue(temp); }
if (MessageLogEnabled) { LogEntries.Enqueue(temp); }
diff --git a/library/PSFramework/PSFCore/PSFCoreHost.cs b/library/PSFramework/PSFCore/PSFCoreHost.cs
index 588db0a6..47e325e5 100644
--- a/library/PSFramework/PSFCore/PSFCoreHost.cs
+++ b/library/PSFramework/PSFCore/PSFCoreHost.cs
@@ -29,5 +29,33 @@ public static string ModuleRoot
}
}
private static string _ModuleRoot;
+
+ ///
+ /// Initializes the PSFramework library.
+ /// Required for some components to work correctly.
+ ///
+ public static void Initialize()
+ {
+ if (_Initialized)
+ return;
+ _Initialized = true;
+
+ // Initialization logic goes here
+ }
+ private static bool _Initialized = false;
+
+ ///
+ /// Reverses the initialization of the PSFramework library.
+ /// Should be called when destroying the main runspace
+ ///
+ public static void Uninitialize()
+ {
+ if (!_Initialized)
+ return;
+
+ // De-Initiialization logic goes here
+
+ _Initialized = false;
+ }
}
}
diff --git a/library/PSFramework/Runspace/RunspaceBoundValue.cs b/library/PSFramework/Runspace/RunspaceBoundValue.cs
index 06fe3847..e3bd1c2c 100644
--- a/library/PSFramework/Runspace/RunspaceBoundValue.cs
+++ b/library/PSFramework/Runspace/RunspaceBoundValue.cs
@@ -1,4 +1,5 @@
-using System;
+using PSFramework.Utility;
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Management.Automation.Runspaces;
@@ -10,7 +11,7 @@ namespace PSFramework.Runspace
///
/// Wrapper class that offers the tools to make Values runspace specific
///
- public class RunspaceBoundValue
+ public class RunspaceBoundValue : IDisposable
{
///
/// Whether the defautl value should be offered when asking from a runspace without custom settings
@@ -49,9 +50,63 @@ public object Value
}
}
+ ///
+ /// Removes all value entries whose corresponding Runspace has been destroyed
+ ///
public void PurgeExpired()
{
- System.Management.Automation.Runspaces.Runspace.GetRunspaces(null);
+ // Store IDs first, so parallel access is not an issue and a new value gets accidentally discarded
+ Guid[] IDs = Values.Keys.ToArray();
+ ICollection runspaces = UtilityHost.GetRunspaces();
+ ICollection runspaceIDs = (ICollection)runspaces.Select(o => o.InstanceId);
+
+ foreach (Guid ID in IDs)
+ if (!runspaceIDs.Contains(ID))
+ Values.Remove(ID);
+ }
+
+ ///
+ /// 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.
+ ///
+ public void Dispose()
+ {
+ Values = new Dictionary();
+ DefaultValue = null;
+ RunspaceHost._RunspaceBoundValues.Remove(this);
+ }
+
+ ///
+ /// Create an empty runspace bound value object
+ ///
+ public RunspaceBoundValue()
+ : this(null, true)
+ {
+
+ }
+
+ ///
+ /// Create a runspace bound value object with its initial value
+ ///
+ /// The object to set as the initial value
+ public RunspaceBoundValue(object Value)
+ : this(Value, true)
+ {
+
+ }
+
+ ///
+ /// 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
+ public RunspaceBoundValue(object Value, bool OfferDefaultValue)
+ {
+ this.Value = Value;
+ this.OfferDefaultValue = OfferDefaultValue;
+
+ // Add to central list of runspacebound values
+ RunspaceHost._RunspaceBoundValues.Add(this);
}
}
}
diff --git a/library/PSFramework/Runspace/RunspaceHost.cs b/library/PSFramework/Runspace/RunspaceHost.cs
index c2725dd2..ddf329f6 100644
--- a/library/PSFramework/Runspace/RunspaceHost.cs
+++ b/library/PSFramework/Runspace/RunspaceHost.cs
@@ -16,5 +16,10 @@ public static class RunspaceHost
/// The dictionary containing the definitive list of unique Runspace
///
public static Dictionary Runspaces = new Dictionary();
+
+ ///
+ /// List of all runspace bound values in use
+ ///
+ internal static List _RunspaceBoundValues = new List();
}
}
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
}
}
diff --git a/library/PSFramework/Utility/UtilityHost.cs b/library/PSFramework/Utility/UtilityHost.cs
index a39c6913..c8ee29ad 100644
--- a/library/PSFramework/Utility/UtilityHost.cs
+++ b/library/PSFramework/Utility/UtilityHost.cs
@@ -235,6 +235,17 @@ public static object GetExecutionContextFromTLS()
return method.Invoke(null, BindingFlags.NonPublic | BindingFlags.Static, null, null, System.Globalization.CultureInfo.CurrentCulture);
}
+ ///
+ /// Returns the list of runspaces available in the process
+ ///
+ /// The lists of currently known runspaces
+ public static ICollection GetRunspaces()
+ {
+ Type runspaceType = typeof(System.Management.Automation.Runspaces.Runspace);
+ MethodInfo method = runspaceType.GetMethod("get_RunspaceList", BindingFlags.Static | BindingFlags.NonPublic);
+ return (ICollection < System.Management.Automation.Runspaces.Runspace > )method.Invoke(null, null);
+ }
+
///
/// Removes an alias from the global list of aliases
///