Use Begin, Process, End in Scriptblock - powershell

Is it possible to use the adavanced function features Begin, Process, End in a script-block?
For example I've the following script block:
$startStopService = {
Param(
[bool] $startService)
if ($startService){
...
Start-Service "My-Service"
}
else {
Stop-Service "My-Service"
}
}
Since I want to be able to control the verbose output of the scriptblock I want to change the block to:
$startStopService = {
Param(
[bool] $startService)
Begin {
$oldPreference = $VerbosePreference
$VerbosePreference = $Using:VerbosePreference
}
Process {
if ($startService){
...
Start-Service "My-Service"
}
else {
Stop-Service "My-Service"
}
}
End {
# Restore the old preference
$VerbosePreference = $oldPreference
}
}
Is it possible to use Begin, Process, End here, though the scriptblock isn't a cmdlet? I simply want that the VerbosePreference gets restored to the old value, regardless an error occurred or not. Of course I could use try{}finally{} as an alternative, but I find that Begin, Process, End is more intuitive.
Thx

It is possible, as described in about_script_blocks:
Like functions, script blocks can include the DynamicParam, Begin,
Process, and End keywords. For more information, see about_Functions
and about_Functions_Advanced.
To test this out, I modified your scriptblock and ran this:
$startStopService = {
Param(
# a bool needs $true or $false passed AFAIK
# A switch is $true if specified, $false if not included
[switch] $startService
)
Begin {
$oldPreference = $VerbosePreference
Write-Output "Setting VerbosePreference to Continue"
# $Using:VerbosePreference gave me an error
$VerbosePreference = "Continue"
}
Process {
if ($startService){
Write-Verbose "Service was started"
}
else {
Write-Verbose "Service was not started"
}
}
End {
# Restore the old preference
Write-Output "Setting VerbosePreference back to $oldPreference"
$VerbosePreference = $oldPreference
}
}
Write-Verbose "This message will not print if VerbosePreference is the default SilentlyContinue"
. $startStopService -startService
Write-Verbose "This message will not print if VerbosePreference is the default SilentlyContinue"
What functionality are you after? If you would to print verbose messages when running a scriptblock but not change the $VerbosePreference in the rest of the script , consider using [CmdletBinding()] and the -Verbose flag:
$startStopService = {
[CmdLetBinding()]
Param(
[switch] $startService
)
Write-Verbose "This is a verbose message"
}
Write-Verbose "This message will not print if VerbosePreference is the default SilentlyContinue"
. $startStopService -verbose
Write-Verbose "This message will not print if VerbosePreference is the default SilentlyContinue"
Edit - Invoke-Command
After your comment, I looking into the functionality of Invoke-Command. And found a lot of things that don't work.
The short version that I believe is most useful to you: you can declare $VerbosePreference = "Continue" within a scriptblock and this will be limited to the scope of the scriptblock. No need to change back after.
$startStopService = {
[CmdLetBinding()]
Param(
[parameter(Position=0)]
[switch]$startStopService,
[parameter(Position=1)]
[switch]$Verbose
)
if($Verbose){
$VerbosePreference = "Continue"
}
Write-Verbose "This is a verbose message"
}
Write-output "VerbosePreference: $VerbosePreference"
Write-Verbose "This message will not print if VerbosePreference is the default SilentlyContinue"
Invoke-Command -Scriptblock $startStopService -ArgumentList ($true,$true)
Write-output "VerbosePreference: $VerbosePreference"
Write-Verbose "This message will not print if VerbosePreference is the default SilentlyContinue"
Trying to pass the -Verbose switch CommonParameter to Invoke-Command was a no-go. This uses a standard Verbose switch parameter that allows you to pass $true/$false (or omit) to control the verbose output.
Related:
about_Functions
about_Functions_Advanced

Related

How to determine success of ScriptBlock?

I want to know if a ScriptBlock executed successfully.
If I run
1/0; echo $?
I get
RuntimeException: Attempted to divide by zero.
false
but if I do
$s = { 1/0 }; Invoke-Command $s; echo $?
I get
RuntimeException: Attempted to divide by zero.
true
I assume that $? is referring to the execution of Invoke-Command, but how can I get success of the script block itself?
I don't need to use Invoke-Command, if changing to $s.Invoke() is viable then happily use that.
Context
I want to write a function for a DSL along the likes of
function at_place {
Param(
[string] $Path,
[scriptblock] $ScriptBlock
)
Push-Location $Path ;
Invoke-Command $ScriptBlock ;
# following line doesn't work
[bool] $ScriptBlockPass = $? ;
If ( $? ){
Write-Debug "success!" ;
Pop-Location ;
} Else {
Write-Error "ScriptBlock failed, remaining at $Path, please fix manually." ;
throw "ScriptBlock failed at $Path" ;
}
}
I have things in my buffer like
Push-Location ~/foo; doStuff; If ( $? ){ Pop-Location; } Else { Write-Error "Failed, fix here" }
Which I would like to write as
at_place ~/foo { doStuff; }
My actual change is about using Git to ignore a bunch of files, stash them, apply some edits, and then reignore them etc. Simplified here hopefully to be more broadly applicable and less distracting.
Your question is a bit broad and the answer is, it really depends.
Child scope invocations can only update the value of $? via $PSCmdlet using either .WriteError method or .ThrowTerminatingError method as stated in the about Automatic Variables documentation. This would imply that the script block or function is an advanced one:
$s = {
[CmdletBinding()]
param()
try {
1 / 0
}
catch {
$PSCmdlet.WriteError($_)
}
}
& $s # Writes Error
$? # Value will be False (Failure)
For a non-advanced function or script block, $? would be categorized as unreliable, one way to know if the script block succeeded (and by succeeded I mean it had no errors) and, in my opinion, the most reliable one, would be to set the error preference to Stop to target both, terminating an non terminating errors and execute the script block in a try / catch / finally:
$s = { Write-Error foo }
try {
$previousPreference = $ErrorActionPreference
$ErrorActionPreference = 'Stop'
& $s
}
catch {
'had errors'
}
finally {
$ErrorActionPreference = $previousPreference
}
As for the new edit, I would approach your function this way:
function at_place {
[CmdletBinding()]
Param(
[string] $Path,
[scriptblock] $ScriptBlock
)
try {
$sbSuccess = $true
$ErrorActionPreference = 'Stop'
$pushSuccess = Push-Location $Path -PassThru
& $ScriptBlock
}
catch {
$sbSuccess = $false
$PSCmdlet.ThrowTerminatingError($_)
}
finally {
if($pushSuccess -and $sbSuccess) {
Pop-Location
}
}
}

How to stop a child process with Ctrl+C without breaking the main script and stream child output to the main script in PowerShell?

I am writing a powershell script that is running on Linux. The purpose of this script is to run the MS SQL Server and show its output, but when the user presses Ctrl+C or some error happens, the script takes a copy of the data folder and then after that exits.
[CmdletBinding()]
PARAM (
[Parameter(ValueFromPipelineByPropertyName)]
[string] $Command,
[Parameter(ValueFromPipelineByPropertyName)]
[string] $Args
)
BEGIN {
Write-Output "started $PSScriptRoot"
$currentErrorLevel = $ErrorActionPreference
# [console]::TreatControlCAsInput = $true
}
PROCESS {
try {
$ErrorActionPreference = 'SilentlyContinue';
$IsSqlRunning = Get-Process -name "sqlservr";
if ($null -eq $IsSqlRunning) {
start-process "/opt/mssql/bin/sqlservr" -wait -NoNewWindow
}
}
catch {
$ErrorActionPreference = $currentErrorLevel;
Write-Error $_.Exception
}
}
End {
$ErrorActionPreference = $currentErrorLevel;
#do backup
Create-Backup "/opt/mssql/bin/data"
Write-Output "finishd!"
}
I got a couple of problems with this script:
When I press Ctrl+C it breaks the main script and it never reaches to the Create-Backup section at the bottom of the script.
If I remove -Wait then the script won't show the sql log output
So my prefered solution is to run the sql with -Wait parameter and prevent the powershell to exit the code after I press Ctrl+C, but instead Ctrl+C close the sql instance
So I'm looking for a way to achieve both.
For simplicity I'll assume that your function only needs to support a single input object, so I'm using a simple function body without begin, process and end blocks, which is equivalent to having just an end block:
[CmdletBinding()]
PARAM (
[Parameter(ValueFromPipelineByPropertyName)]
[string] $Command,
[Parameter(ValueFromPipelineByPropertyName)]
[string] $Args
)
Write-Output "started $PSScriptRoot"
# No need to save the current value, because the modified
# value is local to the function and goes out of scope on
# exiting the function.
$ErrorActionPreference = 'SilentlyContinue';
try {
$IsSqlRunning = Get-Process -name "sqlservr";
if ($null -eq $IsSqlRunning) {
start-process "/opt/mssql/bin/sqlservr" -wait -NoNewWindow
}
}
catch {
Write-Error $_.Exception
}
finally {
# This block is *always* called - even if Ctrl-C was used.
Create-Backup "/opt/mssql/bin/data"
# CAVEAT: If Ctrl-C was used to terminate the run,
# you can no longer produce *pipeline* input at this point
# (it will be quietly ignored).
# However, you can still output to the *host*.
Write-Host "finished!"
}
If you really need to support multiple input objects, it gets more complicated.

Write-Information does not show in a file transcribed by Start-Transcript

I'm using PowerShell 5.1 and I am trying to determine why Write-Information messages do not show in the transcript log created by Start-Transcript unless I set $InformationPreference to SilentlyContinue. I want to both display the messages in the console and have them written to the log file.
I looked here:
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_preference_variables?view=powershell-5.1#informationpreference
Then I decided to create this script to test what gets written and when. See the preference section right underneath Testing explicit behavior with transcripts -------------
Clear-Host
$ErrorActionPreference = "Stop"
try {
Write-Host "Starting transcript"
Start-Transcript -Force -Path "$PSScriptRoot\default.txt"
<#
In PowerShell 5.1 the default behavior is as follows:
$DebugPreference = SilentlyContinue
$InformationPreference = SilentlyContinue
$ProgressPreference = Continue
$VerbosePreference = SilentlyContinue
See the following for more information:
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_preference_variables?view=powershell-5.1
#>
# I am not testing Write-Output as I am not worried about programmatic/pipeline stuff, just contextual messages for end-users or logging
Write-Host "`nTesting default behavior with transcripts --------------------------------`n"
# Setting these just in case I launch this script in a session where a previous script might have modified the preference variables
$DebugPreference = "SilentlyContinue"
$InformationPreference = "SilentlyContinue"
$ProgressPreference = "Continue"
$VerbosePreference = "SilentlyContinue"
Write-Host "Calling Write-Host"
Write-Debug "Calling Write-Debug"
Write-Error "Calling Write-Error" -ErrorAction "Continue"
Write-Information "Calling Write-Information"
Write-Progress "Calling Write-Progress"
Write-Verbose "Calling Write-Verbose"
Stop-Transcript
Start-Transcript -Force -Path "$PSScriptRoot\everything_continue.txt"
Write-Host "`nTesting explicit behavior with transcripts --------------------------------`n"
# Turn everything on
$DebugPreference = "Continue"
$InformationPreference = "Continue" # Setting this to SilentlyContinue makes it show up in the log but not the console. Setting this to 'Continue' makes it show up in the console but not the log.
$ProgressPreference = "Continue"
$VerbosePreference = "Continue"
Write-Host "Calling Write-Host"
Write-Debug "Calling Write-Debug"
Write-Error "Calling Write-Error" -ErrorAction "Continue"
Write-Information "Calling Write-Information"
Write-Progress "Calling Write-Progress"
Write-Verbose "Calling Write-Verbose"
Stop-Transcript
Write-Host "`nResults -------------------------------------------------------------------`n"
# See what actually gets captured and written by the transcriber
$messageTypes = #("Write-Debug", "Write-Error", "Write-Host", "Write-Information", "Write-Verbose")
Write-Host "Default" -ForegroundColor Cyan
$lines = Get-Content "$PSScriptRoot\default.txt"
foreach ($message in $messageTypes) {
if ($lines -like "*Calling $message*") {
Write-Host " $message PRESENT" -ForegroundColor Green
}
else {
Write-Host " $message MISSING" -ForegroundColor Red
}
}
Write-Host "Everything Continue" -ForegroundColor Cyan
$lines = Get-Content "$PSScriptRoot\everything_continue.txt"
foreach ($message in $messageTypes) {
if ($lines -like "*Calling $message*") {
Write-Host " $message PRESENT" -ForegroundColor Green
}
else {
Write-Host " $message MISSING" -ForegroundColor Red
}
}
}
catch {
Write-Host "----------------------------------------------------------------------------------------------------"
Write-Host $_.Exception
Write-Host $_.ScriptStackTrace
Write-Host "----------------------------------------------------------------------------------------------------"
try { Stop-Transcript } catch { }
throw $_
}
What you're seeing is a bug in Windows PowerShell (as of v5.1.17134.590) that has been fixed in PowerShell Core (as of at least v6.1.0 - though other transcript-related problems persist; see this GitHub issue).
I encourage you to report it in the Windows PowerShell UserVoice forum (note that the PowerShell GitHub-repo issues forum is only for errors also present in PowerShell Core).
Here's how to verify if the bug is present in your PowerShell version:
Create a script with the code below and run it:
'--- Direct output'
$null = Start-Transcript ($tempFile = [io.path]::GetTempFileName())
# Note that 'SilentlyContinue' is also the default value.
$InformationPreference = 'SilentlyContinue'
# Produces no output.
Write-Information '1-information'
# Prints '2-Information' to the console.
Write-Information '2-information' -InformationAction Continue
$null = Stop-Transcript
'--- Write-Information output transcribed:'
Select-String '-information' $tempFile | Select-Object -ExpandProperty Line
Remove-Item $tempFile
With the bug present (Windows PowerShell), you'll see:
--- Direct output
2-information
--- Write-Information output transcribed:
INFO: 1-information
That is, the opposite of the intended behavior occurred: the transcript logged the call it should'nt have (because it produced no output), and it didn't log the one it should have.
Additionally, the logged output is prefixed with INFO: , which is an inconsistency that has also been fixed in PowerShell Core.
There is no full workaround, except that you can use Write-Host calls in cases where do you want the output logged in the transcript - but such calls will be logged unconditionally, irrespective of the value of preference variable $InformationPreference (while Write-Host formally provides an -InformationAction common parameter, it is ignored).
With the bug fixed (PowerShell Core), you'll see:
--- Direct output
2-information
--- Write-Information output transcribed:
2-information
The transcript is now consistent with the direct output.

How to propagate -Verbose to module functions?

According to answers like this one and my own experience, Powershell can take care of propagating -Verbose (and -Debug) automatically, which is very convenient. However this stops working when the functions which I want to propagate verbosity to are in a module. Code used for testing this:
Create a directory called Mod somewhere, suppose in c:, and add 2 files:
File c:\Mod\Functions.ps1:
function Show-VerbosityB { [cmdletbinding()]Param()
Write-Output "Show-VerbosityB called"
Write-Verbose "Show-VerbosityB is Verbose"
}
File c:\Mod\Mod.psd1:
#{
ModuleVersion = '1.0.0.0'
NestedModules = #('Functions.ps1')
FunctionsToExport = #('*-*')
}
Now crate the main script, say c:\Foo.ps1:
Import-Module c:\Mod
function Show-VerbosityA { [cmdletbinding()]Param()
Write-Output "Show-VerbosityA called"
Write-Verbose "Show-VerbosityA is Verbose"
}
function Show-Verbosity { [cmdletbinding()]Param()
Write-Output "Show-Verbosity called"
Write-Verbose "Show-Verbosity is Verbose"
Write-Output "Testing propagation"
Show-VerbosityA
Show-VerbosityB
}
Show-Verbosity -Verbose
Results in
PS> . C:\Foo.ps1
Show-Verbosity called
VERBOSE: Show-Verbosity is Verbose
Testing propagation
Show-VerbosityA called
VERBOSE: Show-VerbosityA is Verbose
Show-VerbosityB called
Why is the Write-Verbose in the module's function skipped, why does propagation not behave like it does for Show-VerbosityA? (If I just dot-source Functions.ps1 instead of importing the module, the line VERBOSE: Show-VerbosityB is Verbose is printed). I could make propagation manual by e.g. calling Show-VerbosityB -Verbose:$PSBoundParameters['Verbose']. Or are there other, preferrably shorter, ways? It is quite messy if functions behave differently depending on whether they are part of a module or dot-sourced.
The reason this is happening is because the $VerbosePreference is not propagated when the module is called.
I modified your script to explicitly print the value at the same points you are outputting via Write-Verbose and Write-Output.
This powershell.org post proposes adding this to the module, which worked like a charm for me:
if (-not $PSBoundParameters.ContainsKey('Verbose'))
{
$VerbosePreference = $PSCmdlet.GetVariableValue('VerbosePreference')
}
One of the comments mentions bug report with link (it doesn't exist or I don't have permissions to view)
The issue is discussed in a TechNet post, with a link to a Get-CallerPreferance function that addresses this issue.
Module:
function Show-VerbosityB { [cmdletbinding()]Param()
<# uncomment to get verbose preference from caller, when verbose switch not explicitly used.
if (-not $PSBoundParameters.ContainsKey('Verbose'))
{
$VerbosePreference = $PSCmdlet.GetVariableValue('VerbosePreference')
}
#>
Write-Output "`nShow-VerbosityB called"
Write-output "Global pref: $($global:VerbosePreference)"
Write-output "Script pref: $($script:VerbosePreference)"
Write-output "Effect pref: $VerbosePreference"
Write-Verbose "Show-VerbosityB is Verbose"
}
Caller:
Import-Module C:\Mod
Write-output "On startup: $VerbosePreference"
function Show-VerbosityA { [cmdletbinding()]Param()
Write-Output "`nShow-VerbosityA called"
Write-output "Global pref: $($global:VerbosePreference)"
Write-output "Script pref: $($script:VerbosePreference)"
Write-output "Effect pref: $VerbosePreference"
Write-Verbose "Show-VerbosityA is Verbose"
}
function Show-Verbosity { [cmdletbinding()]Param()
Write-Output "`nShow-Verbosity called"
Write-output "Global pref: $($global:VerbosePreference)"
Write-output "Script pref: $($script:VerbosePreference)"
Write-output "Effect pref: $VerbosePreference"
Write-Verbose "Show-Verbosity is Verbose"
Write-Output "`nTesting propagation"
Show-VerbosityA
Show-VerbosityB
}
Show-Verbosity -Verbose
If you want set from the caller script, try this:
(Get-Module 'ModuleName').SessionState.PSVariable.Set('Global:VerbosePreference', $VerbosePreference )

How to pass -Verbose parameter to a function called inside Invoke-Command?

I have the following function that I am trying to invoke remotely with verbose output, but I don't know how to pass the VerbosePreference correctly?
function TestVerbose()
{
[CmdletBinding()]
Param()
Write-Output "output test"
Write-Verbose "verbose test"
}
Invoke-Command -ComputerName computerB -ScriptBlock ${function:TestVerbose}
The question How to Write-Verbose from Invoke-Command? nicely describes how to write something verbose if I have a non-function scriptblock:
Invoke-Command -ComputerName computerB {$VerbosePreference='Continue'; Write-Verbose "verbose test"}
However, I would like to pass a function and also indicate verbose preference. How to do that?
I have tried combining the function with some inline scriptblock, but it makes the function not run at all:
Invoke-Command -ComputerName computerB -ScriptBlock {$VerbosePreference='Continue'; ${function:TestVerbose}}
kind of a workaround. store the current value of verbosepreference , change it in your function and then reset to original value.
function TestVerbose()
{
[CmdletBinding()]
Param()
begin
{
$VerbosePreference_original = $VerbosePreference
$VerbosePreference = 'continue'
Write-Verbose ('Begin:Original value of VerbosePreference : {0}' -f $VerbosePreference_original)
Write-Verbose ('Begin:New value of VerbosePreference : {0}' -f $VerbosePreference)
}
process
{
Write-Output 'output test'
Write-Verbose 'output test'
}
end
{
Write-Verbose ('END:ReSetting value of VerbosePreference to default : {0}' -f $VerbosePreference_original)
$VerbosePreference = $VerbosePreference_original
}
}