Switch statement not processing - powershell

I'm attempting to use a switch statement to control flow in my PowerShell script, but the switch isn't being processed at all. If I run it interactively (highlight + F8 in ISE), it works without a hitch. I validate the argument, and have tested it thoroughly. If(){} statements process properly. Is there some kind of weird bug with switch statements? Even the Default argument isn't being triggered.
[CmdletBinding()]
Param(
[Parameter(Mandatory=$True)]
[ValidateSet('Install','Uninstall','Verify','Enable','Disable')]
[String]$Switch
)
**FUNCTIONS**
[Int]$R = 324
Start-Transcript 'Path\ESD.log' -Append -Force
Switch ($Switch)
{
"VERIFY" { $R = Verify; Break }
"INSTALL" { $R = Install; Break }
"UNINSTALL" { $R = Uninstall; Break }
"ENABLE" { $R = Enable; Break }
"DISABLE" { $R = Disable; Break }
Default { Write-Host "Unable to match: $Switch" -BackgroundColor Red }
}
Write-Host "Exiting with: $R"
Stop-Transcript
EXIT $R
Output:
PS Path\EnterpriseSiteDiscovery> .\ESD verify
Transcript started, output file is Path\ESD.log
Exiting with: 324
Transcript stopped, output file is Path\ESD.log
PS Path\EnterpriseSiteDiscovery> $LASTEXITCODE
324
Content of log:
**********************
Windows PowerShell transcript start
Start time: 20170705154435
**SYSTEMINFO**
**********************
Transcript started, output file is Path\ESD.log
Exiting with: 324
**********************
Windows PowerShell transcript end
End time: 20170705154436
**********************

It turns out when you are inside a Switch construct, the variable
$Switch value is redefined (presumably by the switch construct itself)
as an empty variable of type System.Collections.IEnumerator. The value
is set to $null. This won’t be a problem if you’re not using a
variable with the name $Switch. Unfortunately I was because I was
working with a set to Virtual Switches so $Switch seemed like a fair
choice of variable name.
https://dscottraynsford.wordpress.com/2015/09/08/powershell-dont-of-the-week-switch-in-a-swtich/

Related

How do I output an error without it being returned on the screen?

Okay, so I'm writing a very basic PowerShell script (I'm on the newbie side here) that does the following:
Checks for the version of a running process (output saved to variable)
If the process is not detected and there is an error, perform an install of program
If the process version is less than or equal to $version, run a command to upgrade the program
My problem is that if the process is not detected, the command errors out and returns to the prompt. What I really want is for this error (or other responses to the command) to -only- be recorded in the variable, not terminate the script. So if I do:
$DesiredVersion = Versionnumber
$ProductVersion = (Get-Process 'process').ProductVersion
if ($ProductVersion -like "*Cannot find*") {
Start-Process InstallProgram.exe
}
elseif ($ProductVersion -lt $DesiredVersion) {
Start-Process UpgradeProgram.exe
}
else {
"Product is up to date"
}
and so on, I want the first command to direct any output of the command into the variable without terminating the script (even in event of error). How would I go about doing this?
I tried doing a Write-Output command to write everything to a text file. This did not resolve things, the command still returns an error and exits.
Just starting out is the perfect time to get into good habits and use Try...Catch constructs:
$DesiredVersion = Versionnumber
Try {
$ProductVersion = (Get-Process 'process' -ErrorAction Stop).ProductVersion
}
Catch {
"Process not found or error:"
# $_ contains the error code
$_
}
# If $ProductVersion has a value then process accordingly
If ($ProductVersion) {
Try {
If ($ProductVersion -lt $DesiredVersion) {
Start-Process UpgradeProgram.exe -ErrorAction Stop
}
Else {
"Product is up to date"
}
}
Catch {
"Error upgrading:"
$_
}
}

Powershell Script working fine in Visual Code but fails running from Terminal

I'm working on writing a script which will run from AzDo Pipeline to disable F5 WebServers. Below script works fine in Visual Code and does disable the server as expected . But when running from the terminal or PS window fails with the below error . Can someone please help.
$ServerInput = 'server1.abc.com'
$BIGIPBaseURL = "https://ser-f5-1.prod.abc.com"
$usr = "nilesh"
$SecurePassword='P#assword'
Write-Host "Starting the Script..."
# Initialize variables
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$BIGIPToken = $null
Write-Host -ForegroundColor Green " done!"
$DisableWebServers = $true
# Initialize functions
Write-Host "Initializing functions..." -NoNewline
$PSVersionTable
function Disable-BIGIPNode([string]$NodeName) {
# servers should use the Disable-BIGIPTelcoNode() function
Write-Host "In the Disable function"
if ($NodeName -match "(?i).*telco.*") {
Write-Host -ForegroundColor Yellow "WARNING: `"$($NodeName.ToUpper().Split('.')[0])`" is in the wrong list. telcoo hosts should be added to the TelcoServers list in your input file."
BREAK
}
else {
if ($BIGIPToken -eq $null) {
Write-Host "Now will enter the Open-Session"
Open-BIGIPSession
}
Write-Host "Disabling node `"$($NodeName.ToUpper().Split('.')[0])`" in BIG-IP..." -NoNewline
$WebRequestInput = #{
body = #{
"session" = "user-disabled"
} | ConvertTo-Json
uri = $($BIGIPBaseURL) + "/mgmt/tm/ltm/node/~Common~" + $NodeName.ToLower()
headers = #{
"Content-Type" = "application/json"
"X-F5-Auth-Token" = "$BIGIPToken"
}
method = "PATCH"
}
Write-Host $WebRequestInput
Write-Host $WebRequestInput.body
try {
Write-Host "In the final try block"
$Request = Invoke-WebRequest #WebRequestInput -UseBasicParsing -SkipCertificateCheck
}
catch {
Write-Host -ForegroundColor Red " failed!"
Write-Host -ForegroundColor Red ($_.ErrorDetails | ConvertFrom-Json).Message
}
Write-Host -ForegroundColor Green " done!"
$global:ZabbixRequestID++
}
}
function Open-BIGIPSession() {
Write-Host "Authenticating with BIG-IP API..." -NoNewline
$WebRequestInput = #{
body = #{
username = "$usr"
password = "$SecurePassword"
loginProviderName = "tmos"
} | ConvertTo-Json
uri = $ScriptInput.BIGIPBaseURL + "/mgmt/shared/authn/login"
headers = #{
"Content-Type" = "application/json"
}
method = "POST"
}
try {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$Request = Invoke-WebRequest #WebRequestInput -UseBasicParsing -SkipCertificateCheck
}
catch {
Write-Host -ForegroundColor Red " failed!"
Write-Host -ForegroundColor Red ($_.ErrorDetails | ConvertFrom-Json).Message
EXIT 1
}
Write-Host -ForegroundColor Green " done!"
$global:BIGIPToken = ($Request.Content | ConvertFrom-Json).token.token
}
if ($DisableWebServers) {
Write-Host "Starting the main Methord "
foreach ($Server in $($ServerInput)) {
Disable-BIGIPNode -NodeName $Server
}
}
The PowerShell version is PSVersion 7.2.2
Disabling node "SAC-DEV-WEB2" in BIG-IP...System.Collections.DictionaryEntry System.Collections.DictionaryEntry System.Collections.DictionaryEntry System.Collections.DictionaryEntry
{
"session": "user-disabled"
}
In the final try block
failed!
ConvertFrom-Json: C:\Temp\Testing.ps1:49:64
Line |
49 | … Host -ForegroundColor Red ($_.ErrorDetails | ConvertFrom-Json).Messag …
| ~~~~~~~~~~~~~~~~
| Conversion from JSON failed with error: Additional text encountered after finished reading JSON content: U. Path '', line
| 3, position 4.
Its working fine when running from VsCode but fails if called with the file name from the same terminal
like .\Testing.ps1
Please help
Your incidental problem is that the true error message is being obscured by a follow-up error that results from attempting to parse the error record's .ErrorDetails property as JSON, which it isn't. (You report that examining the true error reveals a 401 authentication error).
I have no specific explanation for the difference in behavior you're seeing between running in Visual Studio Code vs. in a regular PowerShell console, but I have a guess:
Your Visual Studio Code session in the so-called PowerShell Integrated Console may have lingering state from earlier debugging runs, which may mask a bug in your script.
Restarting Visual Studio Code should clarify whether that is the case, but there's also a way to configure the PowerShell extension so that the problem doesn't arise to begin with - see below.
By default, code you run (debug) via the Visual Code PowerShell extension executes in the same PowerShell session, directly in the global scope.
That is, running a script being edited, say, foo.ps1, in the debugger is effectively the same as invoking it with . .\foo.ps1, i.e. it is in effect dot-sourced.
Therefore, a given debugging run can be affected by earlier runs, because the state of earlier runs lingers.
This can result in bugs going undetected, such as in the following example:
Say your script defines variable $foo and uses it throughout the script. If you debug your script at least one, $foo is now defined in the PowerShell session in the PowerShell Integrated Console.
Say you then change the name to $bar, but you forget to also replace (all) references to $foo with $bar.
Your script is now effectively broken, but you won't notice in the same session, because $foo is still around from earlier debugging runs.
However, running the script from a regular PowerShell console would surface the problem.
The obsolescent Windows PowerShell ISE exhibits the same unfortunate behavior, invariably so, but fortunately there is a solution for the PowerShell extension - see next point.
You can avoid this problem by activating the Create Temporary Integrated Console setting (via File > Preferences > Settings or Ctrl+,), which ensure that every debugging run creates a new, temporary session to run in, which starts with a clean slate:
Whenever a new temporary session is started, any previous one is automatically discarded.
A temporary session has prefix [TEMP] in the list of running shells in the integrated terminal.
You pay a performance penalty, because a new PowerShell session must be created for every run, and you lose the previous session's display output - but I suspect avoiding the pitfalls of lingering state is worth the price.
Note that, in a given temporary session, the dot-sourced invocation described above still applies, but with the lingering-state problem out of the picture, it can now be considered an advantage: After the script finishes, and before the temporary session is replaced with a new one, the variables and functions defined in the script's top-level scope are then available for inspection.

How to implement Invoke-SilentlyAndReturnExitCode as a Powershell module function?

Please, observe:
The method
PS C:\> (Get-Command Invoke-SilentlyAndReturnExitCode).ScriptBlock
param([scriptblock]$Command, $Folder)
$ErrorActionPreference = 'Continue'
Push-Location $Folder
try
{
& $Command > $null 2>&1
$LASTEXITCODE
}
catch
{
-1
}
finally
{
Pop-Location
}
PS C:\>
The command to silence
PS C:\> $ErrorActionPreference = "Stop"
PS C:\> $Command = { cmd /c dir xo-xo-xo }
PS C:\> & $Command > $null 2>&1
cmd : File Not Found
At line:1 char:14
+ $Command = { cmd /c dir xo-xo-xo }
+ ~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (File Not Found:String) [], RemoteException
+ FullyQualifiedErrorId : NativeCommandError
PS C:\>
As you can see, it fails with an exception. But we can silence it easily, right?
PS C:\> $ErrorActionPreference = 'SilentlyContinue'
PS C:\> & $Command > $null 2>&1
PS C:\> $LASTEXITCODE
1
PS C:\>
All is good. Now my function does the same, so let us try it:
PS C:\> $ErrorActionPreference = "Stop"
PS C:\> Invoke-SilentlyAndReturnExitCode $Command
-1
PS C:\>
Yikes! It returns -1, not 1.
The problem appears to be that setting $ErrorActionPreference inside the function does not actually propagate to the command scope. Indeed, let me add some output:
PS C:\> (Get-Command Invoke-SilentlyAndReturnExitCode).ScriptBlock
param([scriptblock]$Command, $Folder)
$ErrorActionPreference = 'Continue'
Push-Location $Folder
try
{
Write-Host $ErrorActionPreference
& $Command > $null 2>&1
$LASTEXITCODE
}
catch
{
-1
}
finally
{
Pop-Location
}
PS C:\> $Command = { Write-Host $ErrorActionPreference ; cmd /c dir xo-xo-xo }
PS C:\> Invoke-SilentlyAndReturnExitCode $Command
Continue
Stop
-1
PS C:\>
So, the problem is really around $ErrorActionPreference - why does it not propagate? Powershell uses dynamic scoping, so the command definition should not capture its value, but use the one from the function. So, what is going on? How to fix it?
tl;dr
Because your Invoke-SilentlyAndReturnExitCode function is defined in a module, you must recreate your script block in the scope of that module for it to see the module-local $ErrorActionPreference value of Continue:
# Use an in-memory module to demonstrate the behavior.
$null = New-Module {
Function Invoke-SilentlyAndReturnExitCode {
param([scriptblock] $Command, $Folder)
$ErrorActionPreference = 'Continue'
Push-Location $Folder
try
{
Write-Host $ErrorActionPreference # local value
# *Recreate the script block in the scope of this module*,
# which makes it see the module's variables.
$Command = [scriptblock]::Create($Command.ToString())
# Invoke the recreated script block, suppressing all output.
& $Command *>$null
# Output the exit code.
$LASTEXITCODE
}
catch
{
-1
}
finally
{
Pop-Location
}
}
}
$ErrorActionPreference = 'Stop'
$Command = { Out-Host -InputObject $ErrorActionPreference; cmd /c dir xo-xo-xo }
Invoke-SilentlyAndReturnExitCode $Command
On Windows, the above now prints the following, as expected:
Continue
Continue
1
That is, the recreated $Command script block saw the function-local $ErrorActionPreference value, and the catch block was not triggered.
Caveat:
This will only work if the $Command script block contains no references to variables in the originating scope other than variables in the global scope.
The alternative to avoid this limitation is to define the function outside of a module (assuming you're also calling it from code that lives outside modules).
Background Information
The behavior implies that your Invoke-SilentlyAndReturnExitCode function is defined in a module, and each module has its own domain of scopes (hierarchy of scopes).
Your $Command script block, because it was defined outside that module, is bound to the default scope domain, and even when executed from inside a module, it continues see the variables from the scope domain in which it was defined.
Therefore, $Command still sees the Stop $ErrorActionPreference value, even though for module-originated code inside the function it would be Continue, due to setting a local copy of $ErrorActionPreference inside the module function.
Perhaps surprisingly, it is still the $ErrorActionPreference in effect inside $Command that controls the behavior, not the function-local value.
With a redirection such as 2>$null for *>$null in effect while Stop is the effective $ErrorActionPreference value, the mere presence of stderr output from an external program - whether it indicates a true error of not - triggers a terminating error and therefore the catch branch.
This particular behavior - where the explicit intent to suppress stderr output triggers an error - should be considered a bug, and has been reported in this GitHub issue.
The general behavior, however - a script block executing in the scope in which it was defined - while non-obvious, is by design.
Note: The remainder of this answer is its original form, which contains general background information that, however, does not cover the module aspect discussed above.
*> $null can be used to silence all output from a command - no need for suppressing the success output stream (>, implied 1>) and the error output stream (2>) separately.
Generally, $ErrorActionPreference has no effect on error output from external programs (such as git), because stderr output from external programs bypasses PowerShell's error stream by default.
There is on exception, however: setting $ErrorActionPreference to 'Stop' actually makes redirections such as 2>&1 and *>$null throw a terminating error if an external program such as git produces any stderr output.
This unexpected behavior is discussed in this GitHub issue.
Otherwise, a call to an external program never triggers a terminating error that a try / catch statement would handle. Success or failure can only be inferred from the automatic $LASTEXITCODE variable.
Therefore, write your function as follows if you define (and call) it outside a module:
function Invoke-SilentlyAndReturnExitCode {
param([scriptblock]$Command, $Folder)
# Set a local copy of $ErrorActionPreference,
# which will go out of scope on exiting this function.
# For *> $null to effectively suppress stderr output from
# external programs *without triggering a terminating error*
# any value other than 'Stop' will do.
$ErrorActionPreference = 'Continue'
Push-Location $Folder
try {
# Invoke the script block and suppress all of its output.
# Note that if the script block calls an *external program*, the
# catch handler will never get triggered - unless the external program
# cannot be found.
& $Command *> $null
$LASTEXITCODE
}
catch {
# Output the exit code used by POSIX-like shells such
# as Bash to signal that an executable could not be found.
127
} finally {
Pop-Location
}
}

How to handle errors for the commands to run in Start-Job?

I am writing an automation script. I had a function which takes either a command or an executable. I had to wait until the command or executable has completed running and return if failed or passed. I also want to write the output to file. I am trying with the Start-Job cmdlet.
My current code:
$job = Start-Job -scriptblock {
Param($incmd)
$ret = Invoke-Expression $incmd -ErrorVariable e
if ($e) {
throw $e
} else {
return $ret
}
} -ArumentList $outcmd
Wait-Job $job.id
"write the output to file using receive-job and return if passed or failed"
This works perfectly fine for commands but for executables irrespective of errorcode the value of $e is null. This falsely shows as passed even though the errorcode is 0.
I tried with errorcode using $LASTEXISTCODE and $?. But $? is true for executables and $LASTEXISTCODE is either null or garbage value for commands. I am out of ideas and struck here.
When in doubt, read the documentation:
$?
Contains the execution status of the last operation. It contains TRUE if the last operation succeeded and FALSE if it failed.
[…]
$LASTEXITCODE
Contains the exit code of the last Windows-based program that was run.
Basically, you need to check both. $? indicates whether the last PowerShell command/cmdlet was run successfully, whereas $LASTEXITCODE contains the exit code of the external program that was last executed.
if (-not $? -or $LASTEXITCODE -ne 0) {
throw '... whatever ...'
} else {
return $ret
}
However, Invoke-Expression is not a very good approach to executing commands. Depending on what you actually want to execute there are probably better ways to do it, with better methods for error handling.

How to run interactive commands in another application window from powershell

I have another command line program which I invoke from my powershell script and would like to run some interactive commands in that window once it is opened from power shell.
In other words - I do a Invoke-Item $link_to_app which opens up the interactive command line for that application and now I would like to use the application specific commands from within powershell scripts.
e.g. app.exe -help to invoke the help command of the app.exe.
Any pointers would help. Thanks!
Try this:
$app = 'app.exe -help'
Invoke-Expression $app
Tested with this and it worked as expected:
$pingTest = 'ping -n 8 127.0.0.1'
Invoke-Expression $pingTest
From your expanded explanation you appear to want to run 2 commands within the same command prompt. This is possible, however, I'm not sure it will work in your scenario. For example:
test1.bat:
echo "hello!"
test2.bat: echo "goodbye!"
$batchTest = "test1.bat && test2.bat"
cmd /c $batchTest
output:
D:\Test>echo "hello!"
"hello!"
D:\Test>echo "goodbye!"
"goodbye!"
Hope this helps.
I'm not sure, but I think what you want is the ability to have a script send input to and receive output from another program, where the other program has "state" that your script needs to be able to interact with. Below is an example of a script that drives CMD.EXE. CMD has state, such as current working directory and environment variables.
Note, that you could do what the other answerer suggested and just start the program, give all the input on the command line, and then do what you need to with the output. However for CMD if you need to make decisions based on the output, and then give CMD more input based on the previous output, you'd have to save and restore the environment and current working directories between each time you executed CMD. The approach below doesn't require that.
However the approach below does have several caveats. First it is dependent on the PS "host". It works (for me) on the command line PS, but not in ISE. This dependency is due to using the Raw host interface to determine if a key is available. Second it is timing dependent, based on the behavior of CMD (or whatever you use instead). You'll see a few sleep commands in the script. I had to experiment a whole lot to get this script to show CMD's output for a particular sub-command when that command was entered, versus CMD giving the output of previous commands after another command was entered. Comment out the sleeps to see what I mean. Third it is easy to hang Powershell. Killing CMD in task manager gets you out of the hung state, which I had to do many times.
You'll see that I added a couple of commands that the script deals with specially. This is to demonstrate that input to command can come from a PS script (versus input from the keyboard).
$global:ver++
if ($ExecutionContext.Host.name -match "ISE Host$") {
write-warning "This script relies on RawUI functionality not implemented in ISE"
return
}
$in = $null
$flExiting = $false
$doDebug = $false
function dot-debug {param($color)
if ($doDebug) {
write-host "." -NoNewline -ForegroundColor $color
}
}
#function dot-debug {param($color) }
$procInfo = new diagnostics.processstartinfo
$procInfo.RedirectStandardOutput=1
$procInfo.RedirectStandardInput=1
$procInfo.RedirectStandardError=1
$procInfo.FileName="cmd.exe"
$procInfo.UseShellExecute=0
$p=[diagnostics.process]::start($procInfo)
$outBuf = new char[] 4096
write-host "Version $ver"
sleep -Milliseconds 300
do {
dot-debug red
# This while loop determines whether input is available from either
# CMD's standard output or from the user typing. You don't want to
# get stuck waiting for input from either one if it doesn't really have input.
:WaitIO while ($true) {
if (-1 -ne $p.StandardOutput.peek()) {
dot-debug yellow
$cnt = $p.StandardOutput.read( $outBuf, 0, 4096)
} else {
dot-debug Gray
if ($host.ui.rawui.KeyAvailable -or $flExiting) {break}
}
$str = $outBuf[0..($cnt-1)] -join ""
write-host "$str" -NoNewline
while (-1 -eq ($rc =$p.StandardOutput.peek())) {
if ($host.ui.rawui.KeyAvailable -or $flExiting) {
break WaitIO
}
dot-debug DarkGray
sleep -milli 200
}
dot-debug cyan
}
dot-debug green
# read-host echoes input, so commands get echoed twice (cmd also echoes)
#
# $host.ui.rawui.ReadKey("NoEcho, IncludeKeyDown") doesn't work on ISE,
# but does work in the PS cli shell
if ($in -ne "exit") {$in = read-host}
if ($in -eq "td") { # toggle debug
$doDebug = -not $doDebug
$p.StandardInput.WriteLine( "echo debug toggled")
sleep -milli 300
continue
}
if ($in -eq "xxx") {
# Example of script driven output being sent to CMD
$p.StandardInput.WriteLine( "echo This is a very long command that I do not want to have to type in everytime I want to use it")
# You have to give CMD enough time to process command before you read stdout,
# otherwise stdout gets "stuck" until the next time you write to stdin
sleep -milli 1
continue
}
if ($in -eq "exit") {
$flExiting = $true
$p.StandardInput.WriteLine($in)
continue
}
foreach ($char in [char[]]$in) {
$p.StandardInput.Write($char)
}
$p.StandardInput.Write("`n")
sleep -milli 1
} until ($p.StandardOutput.EndOfStream)