Progress in PowerShell: a story of Verbosity and different preferences with classes in Scopes and Proxies thrown in
It began, as these items typically do, with somebody complaining. In PowerShell Model 7.2 the output of Invoke-WebRequest -Verbose
and Invoke-RestMethod -Verbose
seem like this:
VERBOSE: GET with 0-byte payload
In all the sooner variations they seem like the model beneath, which is extra useful whenever you’re attempting to debug code that builds URIs:
VERBOSE: GET https://google.com/ with 0-byte payload
A proxy perform will repair that. If two instructions have the identical title an alias beats a perform, which beats a cmdlet, which beats an exterior program. You possibly can specify the total path to an exterior program or cmdlet – for instance Microsoft.PowerShell.UtilityInvoke-RestMethod
so an Invoke-RestMethod
perform can act as a proxy for the cmdlet, something which calls Invoke-RestMethod
will go to the perform, which calls the cmdlet with its totally certified title. PowerShell even has a mechanism to create the perform’s code:
$cmd = Get-Command Invoke-RestMethod
$MetaData = New-Object System.Administration.Automation.CommandMetaData $cmd
[System.Management.Automation.ProxyCommand]::create($MetaData) | clip
(I don’t carry these 3 traces in my head, once I want them I consult with a script Jeffrey Snover wrote way back, a more moderen model is on the PowerShell Gallery.)
I added additional Write-Verbose
calls and tidied up the autogenerated code and posted the end result as a gist.
A module I’m working has a lot of calls to Invoke-RestMethod
however the proxy perform wouldn’t see that I’d specified -Verbose
. So I wanted to analyze.
The -Verbose
change units the worth of $VerbosePreference
within the perform being referred to as; if you happen to thought setting the worldwide $VerbosePreference
to proceed
was the identical as a operating every thing with -Verbose
, attempting the next may shock you:
$VerbosePreference="Proceed"
Invoke-WebRequest "https://google.com" -OutFile delete.me
Copy-Merchandise delete.me delete.too
Invoke-WebRequest
heeds the choice, however Copy-Merchandise
solely prints a message if the -Verbose
change is specified.
My proxy perform would print a verbose message if run with -Verbose
, it would heed the worldwide preference-variable however not the change handed to the perform that referred to as it. The -Affirm
, -Debug
, -ErrorAction
, -InformationAction
, -Verbose
, -WarningAction
, and -WhatIf
switches all set the corresponding preference-variable inside a perform however that wasn’t being inherited when the features in my module referred to as the proxy perform.
I might reproduce this with the 2 features beneath. First, I loaded them from a single .PSM1
file (and issues had been the identical if I pasted them in at a PowerShell immediate)
perform one {
[cmdletBinding()]
param()
Write-Verbose "One calls two"
two
Write-Verbose "Two returned"
}
perform two {
[cmdletBinding()]
param()
Write-Verbose "Two has a message"
}
If I load this, I get three messages:
VERBOSE: One calls two
VERBOSE: Two has a message
VERBOSE: Two returned
Issues change if One
is in its personal module
One -verbose
now simply produces two messages:
VERBOSE: One calls two
VERBOSE: two returned
Setting the worldwide preference-variable on the immediate returns all 3 – as a result of perform two
sees it.
It’s common to imagine that a perform inherits the variables from no matter referred to as it; we are able to see that working with less complicated features
perform three {
$e="ewe"
4
}
perform 4 {
$e
}
If I set $e
and run three
from the immediate
> $e = "eye"
> three
ewe
the idea holds; and a variety of documentation stops there, but when I import three
from a module
> three
eye
The inheritance assumption is certified by a rule that claims what occurs in a module stays within the module.
The query is what can we do about it?
Earlier than it dawned on me that this was a scopes factor and never simply -Verbose
, a search introduced me a clue; the answer is to alter perform two
(or my proxy perform) as follows:
perform two {
[cmdletBinding()]
param($VerbosePreference = $PSCmdlet.GetVariableValue('VerbosePreference')
Write-Verbose "Two has a message"
}
We will solely use $PSCmdlet
if we’ve got both [cmdletBinding()]
or a [parameter()]
ornament – only a aspect notice on that, if you happen to paste in
perform 5 { param ($p) }
perform six { param ([parameter()]$p) }
whenever you attempt to tab-complete parameters for 5
and six
, you’ll see six
will get all of the frequent parameters however 5
doesn’t – [parameter()]
is an implicit [cmdletBinding()]
, though it’s nonetheless good to jot down the latter explicitly.
$PSCmdlet
is offered when the perform is organising its variables. Contained in the perform we’d by no means exchange $x
with the long-winded $PSCmdlet.GetVariableValue("x")
, however in a parameter it’s “use the worth from the scope that referred to as you – even when that scope is a module”.
Now the referred to as perform inherits the choice from its caller. If we specify -Verbose
it takes priority, so nothing breaks copying in $VerbosePreference
. The identical factor applies to -Affirm
and -WhatIf
.
perform seven {
[cmdletBinding(SupportsShouldProcess=$true)]
param()
eight
Write-Host "One thing Secure "
}
perform eight {
[cmdletBinding(SupportsShouldProcess=$true)]
param()
if ($PSCmdlet.ShouldProcess("This","Do you wish to do")) {
Write-Host "One thing harmful"
}
}
If seven
and eight
are loaded from the identical psm1 file
seven -Affirm
Affirm
Are you certain you wish to carry out this motion?
Performing the operation "Do you wish to do" on track "This".
[Y] Sure [A] Sure to All [N] No [L] No to All [S] Droop [?] Assist (default is"Y"): n
One thing Secure
However, if seven
hundreds from its personal module …
seven -Affirm
One thing harmful
One thing Secure
Modifying the parameters in eight
with the code beneath restores the affirmation
param($ConfirmPreference = $PSCmdlet.GetVariableValue('ConfirmPreference'))
-confirm
units $confirmPreference
to low
which triggers $PSCmdlet.ShouldProcess
to immediate the consumer. I deal with -Power
as an additional choice and my features sometimes haveif ($Power -or $PSCmdlet.ShouldProcess(...
to make sure the a command might be run non-interactively, even when its affect is ready larger than $ConfirmPreference
.
The instance above may lead you to assume -Affirm
and -WhatIf
ought to be inherited, however this may trigger an issue with scopes on the script
stage (which apply module-wide). Suppose the module that contained seven learn like this:
$ConfirmPreference="None"
perform seven {
...
}
Any cmdlet that will usually ask for affirmation inside seven
will run silently. However what if the module containing eight
units that choice to one thing totally different? If a perform overrides one thing from its personal script-scope, then solely issues which share that scope see the change, which appears logical. Pondering of features’ scopes as youngsters of their module’s scope which in flip is often a toddler of the worldwide scope means we are able to say issues go down their department of the scope “tree” however don’t soar between branches.
Earlier than leaving $ConfirmPreference
, I’ve been take into consideration altering it inside a perform, so it drops to low
if many gadgets are being up to date. I’m undecided that’s an excellent thought as a result of coming to rely on a immediate and which isn’t there reliably, will result in bother.
The title mentioned that is about Progress. PowerShell 7.2 has an non-obligatory new method of displaying the progress bar, however the issues it depends on are a bit flaky within the Built-in-Shell in Visible Studio Code (issues enhance if you happen to use Write-Progress -completed
) and Invoke-WebRequest
downloading 100 log recordsdata turns into a large number. There isn’t any -Progress
frequent parameter, however the day earlier than I began on the -Verbose
drawback, I had discovered that including $ProgressPreference
as a parameter labored, so it is sensible to do it the identical method. I inserted the next parameter into the perform that calls Invoke-WebRequest
. Not like the examples above, which may be tagged with [Parameter(DontShow)]
I need this to tab-complete the doable values, so it’s marked with the ActionPreference
kind:
[ActionPreference]$ProgressPreference = $PSCmdlet.GetVariableValue('ProgressPreference')
And I may also need that to inherit into my proxy perform.
With that in place, I can use the code beneath to name my perform and exchange the byte-by-byte progress indicator that Invoke-WebRequest
shows with my very own file-by-file one:
$rely = 0
ForEach ($f in $file) {
Write-Progress "Downloading" -PercentComplete ($rely/$file.Depend)
Myfunction $f -ProgressPreference SilentlyContinue
$rely += 100
}
Write-Progress "Downloading" -Accomplished
Fast tip: including 100 to the counter every time (as an alternative of including 1) removes the necessity to multiply by 100 for a share.
Since I discussed ErrorAction
above, earlier than ending I wished to share one final tip about preferences:
perform 9 {
[cmdletBinding()]
param ($Identify)
if (-not $Identify) {throw "Identify is required"}
Write-Host "Deleting $Identify*"
}
The “harmful” line within the instance above by no means runs if $n
is empty, proper? Flawed, truly.
Specifying -ErrorAction
prevents the throw
assertion throwing so
9 "" -ErrorAction SilentlyContinue
would imply the harmful code is run.
When it’s appearing as a “fence” round harmful code, it’s value placing a return
after throw
.