I'm trying to create a quasi-logging function and pass the function parameter as a variable for possible output.
Function Get-Function($continue) {
if (!$error) {
Write-Host "pass"
} else {
$continue
}
}
Get-Function -continue $("$(write-host)success")
If there is an error it outputs success which is listed after the -continue flag.
But this version errors due to the pipeline:
Get-Function -continue $("$(Write-Host)success") | Write-Host "this fails"
It creates an error after the pipeline.
I'm a bit confused as to the question, but it's failing because you're trying to pipe information into a command which you already are giving the parameters. For example:
Works:
"Success" | Write-Host
Write-Host "Success"
Fails:
"Success" | write-host "Success"
If you change that line to just Get-Function -continue "$(Write-Host)success" | Write-Host, it will work but is pointless. You have Write-Host in your function, so there's really no point to write it again. Hope that helps!
Related
I'm writing a script to backup existing bit locker keys to the associated device in Azure AD, I've created a function which goes through the bit locker enabled volumes and backs up the key to Azure however would like to know how I can check that the function has completed successfully without any errors. Here is my code. I've added a try and catch into the function to catch any errors in the function itself however how can I check that the Function has completed succesfully - currently I have an IF statement checking that the last command has run "$? - is this correct or how can I verify please?
function Invoke-BackupBDEKeys {
##Get all current Bit Locker volumes - this will ensure keys are backed up for devices which may have additional data drives
$BitLockerVolumes = Get-BitLockerVolume | select-object MountPoint
foreach ($BDEMountPoint in $BitLockerVolumes.mountpoint) {
try {
#Get key protectors for each of the BDE mount points on the device
$BDEKeyProtector = Get-BitLockerVolume -MountPoint $BDEMountPoint | select-object -ExpandProperty keyprotector
#Get the Recovery Password protector - this will be what is backed up to AAD and used to recover access to the drive if needed
$KeyId = $BDEKeyProtector | Where-Object {$_.KeyProtectorType -eq 'RecoveryPassword'}
#Backup the recovery password to the device in AAD
BackupToAAD-BitLockerKeyProtector -MountPoint $BDEMountPoint -KeyProtectorId $KeyId.KeyProtectorId
}
catch {
Write-Host "An error has occured" $Error[0]
}
}
}
#Run function
Invoke-BackupBDEKeys
if ($? -eq $true) {
$ErrorActionPreference = "Continue"
#No errors ocurred running the last command - reg key can be set as keys have been backed up succesfully
$RegKeyPath = 'custom path'
$Name = 'custom name'
New-ItemProperty -Path $RegKeyPath -Name $Name -Value 1 -Force
Exit
}
else {
Write-Host "The backup of BDE keys were not succesful"
#Exit
}
Unfortunately, as of PowerShell 7.2.1, the automatic $? variable has no meaningful value after calling a written-in-PowerShell function (as opposed to a binary cmdlet) . (More immediately, even inside the function, $? only reflects $false at the very start of the catch block, as Mathias notes).
If PowerShell functions had feature parity with binary cmdlets, then emitting at least one (non-script-terminating) error, such as with Write-Error, would set $? in the caller's scope to $false, but that is currently not the case.
You can work around this limitation by using $PSCmdlet.WriteError() from an advanced function or script, but that is quite cumbersome. The same applies to $PSCmdlet.ThrowTerminatingError(), which is the only way to create a statement-terminating error from PowerShell code. (By contrast, the throw statement generates a script-terminating error, i.e. terminates the entire script and its callers - unless a try / catch or trap statement catches the error somewhere up the call stack).
See this answer for more information and links to relevant GitHub issues.
As a workaround, I suggest:
Make your function an advanced one, so as to enable support for the common -ErrorVariable parameter - it allows you to collect all non-terminating errors emitted by the function in a self-chosen variable.
Note: The self-chosen variable name must be passed without the $; e.g., to collection in variable $errs, use -ErrorVariable errs; do NOT use Error / $Error, because $Error is the automatic variable that collects all errors that occur in the entire session.
You can combine this with the common -ErrorAction parameter to initially silence the errors (-ErrorAction SilentlyContinue), so you can emit them later on demand. Do NOT use -ErrorAction Stop, because it will render -ErrorVariable useless and instead abort your script as a whole.
You can let the errors simply occur - no need for a try / catch statement: since there is no throw statement in your code, your loop will continue to run even if errors occur in a given iteration.
Note: While it is possible to trap terminating errors inside the loop with try / catch and then relay them as non-terminating ones with $_ | Write-Error in the catch block, you'll end up with each such error twice in the variable passed to -ErrorVariable. (If you didn't relay, the errors would still be collected, but not print.)
After invocation, check if any errors were collected, to determine whether at least one key wasn't backed up successfully.
As an aside: Of course, you could alternatively make your function output (return) a Boolean ($true or $false) to indicate whether errors occurred, but that wouldn't be an option for functions designed to output data.
Here's the outline of this approach:
function Invoke-BackupBDEKeys {
# Make the function an *advanced* function, to enable
# support for -ErrorVariable (and -ErrorAction)
[CmdletBinding()]
param()
# ...
foreach ($BDEMountPoint in $BitLockerVolumes.mountpoint) {
# ... Statements that may cause errors.
# If you need to short-circuit a loop iteration immediately
# after an error occurred, check each statement's return value; e.g.:
# if (-not $BDEKeyProtector) { continue }
}
}
# Call the function and collect any
# non-terminating errors in variable $errs.
# IMPORTANT: Pass the variable name *without the $*.
Invoke-BackupBDEKeys -ErrorAction SilentlyContinue -ErrorVariable errs
# If $errs is an empty collection, no errors occurred.
if (-not $errs) {
"No errors occurred"
# ...
}
else {
"At least one error occurred during the backup of BDE keys:`n$errs"
# ...
}
Here's a minimal example, which uses a script block in lieu of a function:
& {
[CmdletBinding()] param() Get-Item NoSuchFile
} -ErrorVariable errs -ErrorAction SilentlyContinue
"Errors collected:`n$errs"
Output:
Errors collected:
Cannot find path 'C:\Users\jdoe\NoSuchFile' because it does not exist.
As stated elsewhere, the try/catch you're using is what is preventing the relay of the error condition. That is by design and the very intentional reason for using try/catch.
What I would do in your case is either create a variable or a file to capture the error info. My apologies to anyone named 'Bob'. It's the variable name that I always use for quick stuff.
Here is a basic sample that works:
$bob = (1,2,"blue",4,"notit",7)
$bobout = #{} #create a hashtable for errors
foreach ($tempbob in $bob) {
$tempbob
try {
$tempbob - 2 #this will fail for a string
} catch {
$bobout.Add($tempbob,"not a number") #store a key/value pair (current,msg)
}
}
$bobout #output the errors
Here we created an array just to use a foreach. Think of it like your $BDEMountPoint variable.
Go through each one, do what you want. In the }catch{}, you just want to say "not a number" when it fails. Here's the output of that:
-1
0
2
5
Name Value
---- -----
notit not a number
blue not a number
All the numbers worked (you can obvious surpress output, this is just for demo).
More importantly, we stored custom text on failure.
Now, you might want a more informative error. You can grab the actual error that happened like this:
$bob = (1,2,"blue",4,"notit",7)
$bobout = #{} #create a hashtable for errors
foreach ($tempbob in $bob) {
$tempbob
try {
$tempbob - 2 #this will fail for a string
} catch {
$bobout.Add($tempbob,$PSItem) #store a key/value pair (current,error)
}
}
$bobout
Here we used the current variable under inspection $PSItem, also commonly referenced as $_.
-1
0
2
5
Name Value
---- -----
notit Cannot convert value "notit" to type "System.Int32". Error: "Input string was not in ...
blue Cannot convert value "blue" to type "System.Int32". Error: "Input string was not in a...
You can also parse the actual error and take action based on it or store custom messages. But that's outside the scope of this answer. :)
My main PowerShell code runs a function that logs to the Windows eventlog. If the level is error it uses a separate event ID which then our monitoring will pick up that exact ID and run an action. However, if I want to specify in the parameter of the main script (not the function) that this time running it use a different Event ID so it will NOT action monitoring, I don't know where to even start on that.
Is there a way to provide a switch parameter in the main script like $NoAlert which then changes the Event ID in the function?
The function of logging lives in a PowerShell module I created. I am importing the module at the beginning of the script and then calling the function during the main script body.
Here is the function:
function WriteLog-SRTProd {
Param(
[string]$logT,
[Parameter(Mandatory=$true)][string]$level,
[String]$LogFileDirT = "\\ServerA\Logs"
)
$RSLogfileT = (Get-ChildItem -Path $LogFileDirT |
sort LastWriteTime |
select -Last 1).Name
## make sure a level is correctly selected (mandatory)
if ("Error","Info","Warn" -NotContains $Level) {
throw "$($Environment) is not a valid name! Please use 'Error', 'Warn', or 'Info'"
}
if ($Level -eq "Info") {
Add-Content -Path "$LogFileDirT\$RSLogFileT" -Value "$(Get-Date -format MM-dd-yyyy::HH:mm:ss) INFO $logT"
Write-EventLog -LogName Application -Source TEST_MAINT -EntryType Information -EventId 100 -Message $logT -Category 0
}
if ($Level -eq "Warn") {
Add-Content -Path "$LogFileDirT\$RSLogFileT" -Value "$(Get-Date -format MM-dd-yyyy::HH:mm:ss) WARN $logT"
Write-EventLog -LogName Application -Source TEST_MAINT -EntryType Warning -EventId 200 -Message $logT -Category 0
}
if ($Level -eq "Error") {
Add-Content -Path "$LogFileDirT\$RSLogFileT" -Value "$(Get-Date -format MM-dd-yyyy::HH:mm:ss) ERROR $logT"
Write-EventLog -LogName Application -Source TEST_MAINT -EntryType Error -EventId 300 -Message $logT -Category 0
}
}
I'd like to run my script like this. When the $NoAlert is passed, it will send that switch to the function. Is this possible? Can I just add the switch in both places and use an if statement in the function for when the NoAlert switch is used?
PS C:\> .\Maintenance.ps1 -NoAlert
Param(
[switch]$NoAlert
)
WriteLog-SRTProd -level Error -logT "Custom Error Message"
I have created own function for logging and stored/installed as module, below is the part of my log module :
you can customize the write statements and add your code for event log. I have added 'NoAction' enum member as per your requirements.
I have used one Enum to separate the log levels
Enum Severity
{
Error = 3
Warning = 4
Informational = 6
Debug = 7
Verbose = 8
NoAction = 0 # AS PER YOUR REQUIREMENTS
}
function Write-Log()
{
[cmdletbinding()]
param
(
[Parameter(Position=0,mandatory=$true)]
[Severity] $LogLevel,
[Parameter(Position=1,mandatory=$true)]
[String] $Message
)
$TimeStamp = "$(Get-Date -format HH:mm:ss)" ;
Switch($LogLevel)
{
([Severity]::Error.ToString())
{
Write-Error "`t$TimeStamp : $Message`n" -ErrorAction Stop
break;
}
([Severity]::Warning.ToString())
{
Write-Warning "`t$TimeStamp : $Message`n" -WarningAction Continue
break;
}
([Severity]::Informational.ToString())
{
Write-Information "INROMATION:`t$TimeStamp : $Message`n" -InformationAction Continue
break;
}
([Severity]::Verbose.ToString())
{
Write-Verbose "`t$TimeStamp : $Message`n"
break;
}
([Severity]::NoAction.ToString())
{
Write-Verbose "`t$TimeStamp : $Message`n"
break;
}
} # END OF SWITCH
} # END OF FUNCTION
Sample Call :
Write-Log -LogLevel ([Severity]::Informational) -Message "test log message using info level"
Output :
INROMATION: 09:40:15 : test log message using info level
I have decided to just add a new parameter to both function and main script named $NoAlert. I have added an If($NoAlert){WriteLog-SRPProd -NoAlert} to the main script (messy, but its what I needed done). then in the Function, If($NoAlert){EventID 111}. so basically I am using the switch in the main script that then calls the NoAlert switch in the function. This is all done with a few added If/Else statements.
Hopefully that makes sense. Like I said its not the best answer, but I wanted to get it done and still provide an answer here in this post.
I am trying to do validation. checking the deployed values with the given values. I extract the vnet values from Azure resources using RestAPI method and convertto-json from Object because of vnet object is giving me empty object (#{value=System.Object[]}). The following is the Json code I am getting:
{
"value": [
{
"properties": "#{virtualNetworkSubnetId=/subscriptions/<XXXX>/resourceGroups/<XXXX>/providers/Microsoft.Network/virtualNetworks/<XXXX>/subnets/<XXXX>; ignoreMissingVnetServiceEndpoint=True; state=Ready}",
"id": "/subscriptions/<XXXX>/resourceGroups/<XXXX>/providers/Microsoft.DBforPostgreSQL/servers/<XXXX>/virtualNetworkRules/<XXXX>",
"name": "<XXXX>",
"type": "Microsoft.DBforPostgreSQL/servers/virtualNetworkRules"
}
]
}
The following powershell command is to compare the value but getting an error saying $vnet.name and $vnet.id is $null
$vnet= ( $vnet | ConvertTo-Json)
It "has this number of vNet Rules defined: $($config.vnetRules.count)"
{
$vnet.count | Should -Be $config.vnetRules.count
}
#$vnet.count is working and giving an success message
foreach ($vnetRule in $config.vNetRules) {
Write-Host $vnet #-> getting Json
Write-Host $vnet.Name #-> return as Empty($null)
Write-Host $vnet.value.Name #-> return as Empty($null)
Write-Host $vnet.id #-> return as Empty($null)
Write-Host $vnet.value.id #-> return as Empty($null)
it "has a vNet rule named: $($vnetRule.ruleName)" {
$vnet.name | Should -Be $vnetRule.ruleName
}
it "has a vNet Rule Subnet ID of: $($vNetRule.subnetId)" {
$vnet.value.id | Should -Be $vNetRule.subnetId
}
}
Returns $null.
In my attempt to recreate your invoke-restmethod output, I used your JSON packet as input to create the $vnet variable.
I believe the problem is you are using Write-host to display the object instead of simply the object name. Write-host will attempt to convert the complex object to a string and hence you see the weird output as you can see below. see the difference when i simply out the object?
Now, $vnet has 4 properties id, name, properties, type and can be invoked as shown.
In you case, you have converted the variable $vnet to json and then attempting to display its properties. But Json does not have any properties, except length. And hence, invoking those properties will give you null.
Say I have this simple PowerShell function:
function testit() {
return $true > $null
}
Write-Host "testing"
$thistest = testit
Write-Host "value = $thistest"
When I use it in my PowerShell script, I want to receive the value in the script but I don't want it to show in the console.
How do I keep the return value in the pipeline but just hide it from console?
If I use the > $null then it suppresses the output completely - I just want it to not show in the console, but I still want the value.
As documented PowerShell functions return all non-captured output to the caller. If the caller doesn't do anything with the returned value PowerShell automatically passes it to Out-Default, which then forwards it to Out-Host (see this article written by Don Jones).
Using redirection operators on the return value inside the function effectively suppresses the return value so that the function wouldn't return anything.
If you have a function like this:
function testit {
return $true
}
and call it by itself:
testit
PowerShell implicitly does this:
testit | Out-Default
which effectively becomes
testit | Out-Host
If you capture the return value in a variable
$thistest = testit
the value gets stored in the variable without anything being displayed on the console.
If you redirect the output or pipe it into Out-Null
testit >$null
testit | Out-Null
the return value is discarded and nothing is displayed on the console.
If you want to prevent PowerShell's default behavior of passing uncaptured output at the end of a pipeline to Out-Host you can do so by overriding Out-Default like this:
filter Out-Default { $_ | Out-Null }
or (as #PetSerAl pointed out in the comments) like this:
filter Out-Default {}
However, beware that this modification disables Out-Default for everything in the current scope until you remove the filter again. If you do for instance a Get-ChildItem while the filter is active nothing will be displayed unless you explicitly write the output to the host console:
Get-ChildItem | Out-Host
You remove the filter like this:
Remove-Item function:Out-Default
Early on in my script I have check to define whether a parameter "-Silent" was used when running the script. The idea is to have zero output from the script if it was and it will be checked on every Write-Host entry I have later. It seems a bit heavy to make if-else statements on every single Write-Host I have, so I decided to go with a function - something like this:
Function Silent-Write ([string]$arg1)
{
if ($silent -eq $false) {
if ($args -ieq "-nonewline") {
Write-Host "$arg1" -NoNewLine
}
elseif ($args -ieq "-foregroundcolor") {
Write-Host "$arg1" -ForegroundColor $args
}
else {
Write-Host "$arg1"
}
}
}
Silent-Write -ForegroundColor red "hello"
This is not working, but you get the idea; besides passing the text I want to output, Silent-Write function should also take other Write-Host arguments into consideration. Quite simple issue I believe, but something I cannot figure out with the knowledge of functions I have.
In PowerShell V3 you can use splatting:
Function Silent-Write
{
if (!$silent) {
Write-Host #args
}
}
Silent-Write -ForegroundColor red "hello"