Is it possible to customize the verbose message prefix in Powershell? - powershell

The current verbose message prefix is simply VERBOSE:
I would like to modify it to VERBOSE[N]:, where N is the current thread Id.
Is it possible?

This behavior (or rather the format string) is hard-coded into the default PowerShell host and there are no hooks to override it. You'd have to implement your own host or modify the calling code to use a proper logging framework, neither of which are particularly simple.
If you at least control the outermost invocation, you have the option to redirect the verbose stream output, and we can use this in combination with a cmdlet to "sort of" customize things:
function Verbosi-Tee {
[CmdletBinding()]
Param (
[Parameter(ValueFromPipeline = $true)]
$o
)
Process {
if ($o -is [System.Management.Automation.VerboseRecord]) {
Write-Verbose "[$([System.Threading.Thread]::CurrentThread.ManagedThreadId)] $($o.Message)"
} else {
$o
}
}
}
Sample use:
$VerbosePreference = "continue"
$x = (&{
Write-Verbose "This is verbose."
Write-Output "This is just regular output."
} >4&1 | Verbosi-Tee) # redirect, then pipe
"We captured the output in `$x: $x"
Output (on my system):
VERBOSE: [16] This is verbose.
We captured the output in $x: This is just regular output.
The name of the cmdlet is a lie because this doesn't in fact implement a full tee, but a good pun is its own reward.

Related

ValidateScript and order of operations

I am not understanding why ValidateScript is not erroring out until the end..
param (
[Parameter(
Mandatory = $true,
HelpMessage = "Specifies the email address that will manage the share drive to be created.")]
[ValidateScript({
if ($_.host -eq "domain.com") {
return $true
}
else {
Throw [System.Management.Automation.ValidationMetadataException] "Enter an email address ending with #domain.com"
}
})][Net.Mail.MailAddress]$EmailAddress,
[Parameter(
Mandatory = $true,
HelpMessage = "Specifies the name of the share drive name to be created. Enter a shared drive name like prgm- and at least 2 letters. Example prgm-hi")]
[ValidateScript({
if ($_ -like "prgm-??*") {
return $true
}
else {
Throw [System.Management.Automation.ValidationMetadataException] "Enter a shared drive name like prgm- and at least 2 letters. Example prgm-hi"
}
})][string]$SharedDriveName
)
In this code snip sample above if the script is run the person will get past the emailaddress variable if it is wrong and than get to the error after all the variables are answered.
I am not understanding how to have the script stop right as someone makes a bad entry.
I assume, I will have to use something other than validate script but I am not sure what?
Perhaps surprisingly, when PowerShell automatically prompts for values for missing mandatory parameters (written as of PowerShell 7.3.2):
it enforces the parameter data type after each prompt.
but performs validation - based on attributes such as ValidateScript() - only after ALL prompts have been answered.
This makes for an awkward interactive experience, given that as a user you won't learn whether any of your inputs are valid until all of them have been provided.
I can't speak to the design intent - a conceivable reason for this evaluation order is to allow validation script blocks to also consider other parameter values; however, in practice you can not refer to other parameters inside a [ValidateScript()] script block.
On a general note, the automatic prompting mechanism has fundamental limitations - see GitHub issue #7093.
Workaround:
You can move prompting and validating into the body of your function (as well), where you get to control the execution flow - obviously, this requires extra work:
param (
[Parameter(
# Do NOT make the parameter mandatory, so that it can be prompted for
# in the function body, but DO define a HelpMessage.
HelpMessage = "Specify the email address that will manage the share drive to be created (must end in #sonos.com)"
)]
[ValidateScript({
if ($_.host -eq 'sonos.com') {
return $true
}
else {
Throw "Enter an email address ending with #sonos.com"
}
})]
[Net.Mail.MailAddress] $EmailAddress,
[Parameter(
# Do NOT make the parameter mandatory, so that it can be prompted for
# in the function body, but DO define a HelpMessage.
HelpMessage = "Specify the name of the share drive name to be created. Enter a shared drive name like prgm- and at least 2 letters. Example prgm-hi"
)]
[ValidateScript({
if ($_ -like "prgm-??*") {
return $true
}
else {
Throw "Enter a shared drive name like prgm- and at least 2 letters. Example prgm-hi"
}
})]
[string]$SharedDriveName
)
# If no values were provided any of the conceptually mandatory parameters,
# prompt for and validate them now.
# Infer which parameters are conceptually mandatory from the presence of a
# .HelpMessage property.
$MyInvocation.MyCommand.Parameters.GetEnumerator() | ForEach-Object {
# See if there's a .HelpMessage property value and, if so,
# check if the parameter hasn't already been bound.
$helpMessage = $_.Value.Attributes.Where({ $_ -is [Parameter] }).HelpMessage
if ($helpMessage -and -not $PSBoundParameters.ContainsKey($_.Key)) {
# Prompt now and validate right away, which happens implicitly when
# the parameter variable is assigned to.
while ($true) {
try {
Set-Variable -Name $_.Key -Value (Read-Host $helpMessage)
break # Getting here means that validation succeeded -> exit loop.
}
catch {
# Validation failed: write the error message as a warning, and keep prompting.
Write-Warning "$_"
}
}
}
}
# ... all required arguments are now present and validated.
Note:
The upside of this approach is that you can keep prompting until valid input is received (or the user aborts with Ctrl-C).

Appropriate logging in Powershell

If I have a powershell script say called caller.ps1 which looks like this
.\Lib\library.ps1
$output = SomeLibraryFunction
where library.ps1 looks like the following
function SomeLibraryFunction()
{
Write-Output "Some meaningful function logging text"
Write-Output "return value"
}
What I'd like to achieve is a way in which the library function can return it's value but also add some logging messages that allow the caller to process those internal messages as they see fit. The best I can think of is to write both to the pipeline and then the caller will have an array with the actual return value plus the internal messages which may be of use to a logger the calling script has.
Am I going about this problem the right way? Is there a better way to achieve this?
It's usually not a good idea to mix logging messages with actual output. Consumers of your function then have to do a lot of filtering to find the object they really want.
Your function should write logging messages to the verbose output stream. If the caller wants to see those messages, it can by specifying the -Verbose switch to your function.
function SomeLibraryFunction()
{
[CmdletBinding()]
param(
)
Write-Verbose "Some meaningful function logging text"
Write-Output "return value"
}
In PowerShell 3+, consumers can redirect your verbose messages to the output stream or a file:
# Show verbose logging messages on the console
SomeLibraryFunction -Verbose
# Send verbose logging messages to a file
SomeLibraryFunction -Verbose 4> verbose.txt
# redirect verbose message to the output stream
SomeLibraryFunction -Verbose 4>&1
Other options include:
Writing to a well-known file
Writing to the Event Log
Use Start-Transcript to create a log of the session.
Something like Tee-Object might be helpful to you
function SomeLibraryFunction()
{
$filepath = "C:\temp\log.txt"
write-output "Some meaningful function logging text" | Tee-Object -FilePath $filepath -Append
write-output "return value" | Tee-Object -FilePath $filepath -Append
}
For more information on Tee-Object look here
You could use an If statement based on a variable like $logging=$true but i could see that getting messy.
Another Approach
If you are looking for more of an optional solution then maybe you could use something like this Start-Transcript and Stop-Transcript which creates a record of all or part of a Windows PowerShell session in a text file.
function SomeLibraryFunction()
{
write-output "Some meaningful function logging text"
write-output "return value"
}
$logging = $True
If ($logging){
Start-Transcript -Path C:\temp\log.txt -Append
}
SomeLibraryFunction
If ($logging){
Stop-Transcript
}
This would just show that you could toggle the Start and Stop. You could even set the switch with a paramater passed to the script if you chose.
NOTE The output might be more that you are looking for but at least give it a try. Also, this will not work in the Powershell ISE as you will get an error Start-Transcript : This host does not support transcription.
Another way to do this would be to return a compound object that includes the results and the log information. This is then easy to pick apart.
function MyFunc
{
# initialize
$result = $null
$log = #()
# write to the log
$log += "This will be written to the log"
$log += "So will this"
# record the result
$result = $true
# return result and log combined into object
return New-Object -TypeName PSObject -Property #{ result = $result; log = $log -join "`r`n" }
}
# Call the function and get the results
$MyFuncResult = MyFunc
# Display the returned result value
Write-Host ( "MyFunc Result = {0}" -f $MyFuncResult.Result )
# Display the log
write-host ( "MyFunc Log = {0}" -f $MyFuncResult.Log )
Alternatively, if you want to avoid the object, pass in a log variable by reference. The function can write to the log variable and the changes will be visible in the calling scope. To do this, you need to add the [ref] prefix to the function definition AND the function call. When you write to the variable in the function you need to refer to the .value property.
function MyFunc2 ([ref]$log)
{
# initialize
$result = $null
# write to the log
$log.value += "`r`nThis will be written to the log"
$log.value += "`r`nSo will this"
# record the result
$result = $true
# return result and log combined into object
return $result
}
# Call the function and get the results
$log = "before MyFunc2"
$MyFuncResult = MyFunc2([ref]$log)
$log += "`nafter MyFunc2"
# Display the returned result value
write-host ( "MyFunc2 result = {0}" -f $MyFuncResult )
# Display the log
write-host ( "MyFunc2 Log = {0}" -f $Log )

Capturing verbose output from -Verbose switch in PowerShell v2

I ran into an interesting issue today that has me puzzled. I need to capture output from the verbose stream and write it to stdout.
This can be accomplished with this:
# Create a PowerShell Command
$pscmd = [PowerShell]::Create()
# Add a Script to change the verbose preference.
# Since I want to make sure this change stays around after I run the command I set UseLocalScope to $false.
# Also note that since AddScript returns the PowerShell command, I can simply call Invoke on what came back.
# I set the return value to $null to suppress any output
$null = $psCmd.AddScript({$VerbosePreference = "Continue"},$false).Invoke()
# If I added more commands, I'd be adding them to the pipeline, so I want to clear the pipeline
$psCmd.Commands.Clear()
# Now that I've cleared the pipeline, I'll add another script that writes out 100 messages to the Verbose stream
$null = $psCmd.AddScript({Write-verbose "hello World" }).Invoke()
# Finally, I'll output the stream
$psCmd.Streams.Verbose
Now the interesting part is if I were to create a function called Hello-World and use [CmdletBinding()] to inherit the -verbose switch, I can no longer capture output:
Function Hello-World {
[CmdletBinding()]
Param()
Write-Verbose "hello world"
}
...
$null = $psCmd.AddScript({Hello-World -Verbose }).Invoke()
...
I am assuming that the function is given its own verbose stream and that visibility to the stream is lost, but I am not positive. Does this have to do with [CmdletBinding()]?
Avoiding transcripts, is there a way to accomplish this?
Thanks!
Thank you #JasonMorgan, below is the solution that appears to be working. I needed to declare the function in the pscmd I made:
$pscmd = [PowerShell]::Create()
$null = $psCmd.AddScript({$VerbosePreference = "Continue"},$false).Invoke()
$null = $psCmd.AddScript({
function Hello-World {
[CmdletBinding()]
Param()
Write-Verbose "hello world"
}
}, $false).Invoke()
$psCmd.Commands.Clear()
$null = $psCmd.AddScript({Hello-World -Verbose }).Invoke()
$psCmd.Streams.Verbose

Alternative to Throwing Param Exceptions in PowerShell?

Bottom Line Up Front
I'm looking for a method to validate powershell (v1) command line parameters without propagating exceptions back to the command line.
Details
I have a powershell script that currently uses param in conjunction with [ValidateNotNullOrEmpty] to validate command line paramaters:
param(
[string]
[ValidateNotNullOrEmpty()]$domain = $(throw "Domain (-d) param required.")
)
We're changing the paradigm of error handling where we no longer want to pass exceptions back to the command line, but rather provide custom error messages. Since the param block can not be wrapped in a try catch block, i've resorted to something like the following:
param(
[string]$domain = $("")
)
Try{
if($domain -like $("")){
throw "Domain (-d) param required."
}
...
}Catch{
#output error message
}
My concern is that we're bypassing all of the built-in validation that is available with using param. Is my new technique a reasonable solution? Is there a better way to validate command line params while encapsulating exceptions within the script? I'm very much interested in see how PowerShell professionals would handle this situation.
Any advice would be appreciated.
You can write a custom validation script. Give this parameter a try.
Param(
[ValidateScript({
If ($_ -eq $Null -or $_ -eq "") {
Throw "Domain (-d) param required."
}
Else {
$True
}
})][string]$Domain
)
As I mentioned in a comment: more I read your description, more I come to the conclusion that you should not worry about "bypassing all built-in validation". Why? Because that's exactly your target. You want to bypass it's default behavior, so if that's what you need and have to do - than just do it. ;)
One way is to use default parameters like this [from msdn] -
Function CheckIfKeyExists
{
Param(
[Parameter(Mandatory=$false,ValueFromPipeline=$true)]
[String]
$Key = 'HKLM:\Software\DoesNotExist'
)
Process
{
Try
{
Get-ItemProperty -Path $Key -EA 'Stop'
}
Catch
{
write-warning "Error accessing $Key $($_.Exception.Message)"
}
}
}
So, here, if you try calling the function without passing any parameters, you will get warning what you have defined in your try/catch block. And, you are not using any default validation attributes for that. You should always assume that you will encounter an error, and write code that can survive the error. But the lesson here is if you implement a default value, remember that it is not being validated.
Read more here

Handling pipeline and parameter input in a Powershell function

I'm confused about something I saw in the book Learn PowerShell in a Month of lunches. In chapter 21 when the author discusses functions that accept input via parameter binding or the pipeline he gives two patterns.
The first as follows
function someworkerfunction {
# do some work
}
function Get-SomeWork {
param ([string[]]$computername)
BEGIN {
$usedParameter = $False
if($PSBoundParameters.ContainsKey('computername')) {
$usedParameter = $True
}
}
PROCESS {
if($usedParameter) {
foreach($computer in $computername) {
someworkerfunction -computername $comptuer
}
} else {
someworkerfunction -comptuername $_
}
}
END {}
}
The second like this
function someworkerfunction {
# do stuff
}
function Get-Work {
[CmdletBinding()]
param(
[Parameter(Mandatory=$True,
ValueFromPipelineByPropertyName=$True)]
[Alias('host')]
[string[]]$computername
)
BEGIN {}
PROCESS {
foreach($computer in $computername) {
someworkerfunction -comptuername $computer
}
}
END {}
}
I know the second sample is a standard Powershell 2.0 Advanced function. My question is with Powershell 2.0 support for the cmdletbinding directive would you ever want to use the first pattern. Is that just a legacy from Powershell 1.0? Basically is there ever a time when using Powershell 2.0 that I would want to mess around with the first pattern, when the second pattern is so much cleaner.
Any insight would be appreciated.
Thank you.
If you want to process pipeline input in your function but don't want to add all the parameter attributes or want backwards compatibility go with the cmdletbindingless way.
If you want to use the additional features of PowerShell script cmdlets like the parameter attributes, parameter sets etc... then go with the second one.
If anyone wishes for a very, very simple explanation of how to read from piped input see
How do you write a powershell function that reads from piped input?
Had this ^ existed when I had this question, I would have saved a lot of time because this thread is quite complicated and doesn't actually explain how to handle pipelined input into a function.
No, the first example is not just legacy. In order to create a PowerShell function that uses an array parameter and takes pipeline input you have to do some work.
I will even go as far as to say that the second example does not work. At least I could not get it to work.
Take this example...
function PipelineMadness()
{
[cmdletbinding()]
param (
[Parameter(Mandatory = $true, ValueFromPipeline=$true)]
[int[]] $InputArray
)
Write-Host ('$InputArray.Count {0}' -f $InputArray.Count)
Write-Host $InputArray
Write-Host ('$input.Count {0}' -f $input.Count)
Write-Host $input
if($input) { Write-Host "input is true" }
else { Write-Host "input is false" }
}
results ...
PS C:\Windows\system32> 1..5 | PipelineMadness
$InputArray.Count 1
5
$input.Count 5
1 2 3 4 5
input is true
PS C:\Windows\system32> PipelineMadness (1..5)
$InputArray.Count 5
1 2 3 4 5
$input.Count 1
input is false
Notice that when the pipeline is used the $InputArray variable is a single value of 5...
Now with BEGIN and PROCESS blocks
function PipelineMadnessProcess()
{
[cmdletbinding()]
param (
[Parameter(Mandatory = $true, ValueFromPipeline=$true)]
[int[]] $InputArray
)
BEGIN
{
Write-Host 'BEGIN'
Write-Host ('$InputArray.Count {0}' -f $InputArray.Count)
Write-Host $InputArray
Write-Host ('$input.Count {0}' -f $input.Count)
Write-Host $input
if($input) { Write-Host "input is true" }
else { Write-Host "input is false" }
}
PROCESS
{
Write-Host 'PROCESS'
Write-Host ('$InputArray.Count {0}' -f $InputArray.Count)
Write-Host $InputArray
Write-Host ('$input.Count {0}' -f $input.Count)
Write-Host $input
if($input) { Write-Host "input is true" }
else { Write-Host "input is false" }
}
}
Now this is where it gets weird
PS C:\Windows\system32> 1..5 | PipelineMadnessProcess
BEGIN
$InputArray.Count 0
$input.Count 0
input is false
PROCESS
$InputArray.Count 1
1
$input.Count 1
1
input is true
PROCESS
$InputArray.Count 1
2
$input.Count 1
2
input is true
...
PROCESS
$InputArray.Count 1
5
$input.Count 1
5
input is true
The BEGIN block does not have any data in there at all. And the process block works well however if you had a foreach like the example it would actually work but it would be running the foreach with 1 entry X times. Or if you passed in the array it would run the foreach once with the full set.
So I guess technically the example would work but it may not work the way you expect it to.
Also note that even though the BEGIN block had no data the function passed syntax validation.
To answer your question, I would say that the first pattern is just a legacy from PowerShell 1.0, you also can use $input in classical functions without Process script block. As far as you are just writting code for PowerShell 2.0 you can forget it.
Regarding pipeline functions, in powerShell V1.0 they can be handled with filters.
you just have to know that it have been done like that when you take samples from the Net or when you have to debug old Powerhell code.
Personally I still use old functions and filters inside my modules I reserve cmdletbinding for export functions or profile functions.
Powershell is a bit like lego blocks, you can do many things in many different ways.
The first form is expecting one or more computer names as string arguments either from an argumentlist or from the pipeline.
The second form is expecting either an array of string arguments from an argument list, or input objects from the pipeline that have the computer names as a property.