How to Output value from function to caller but not to console - function

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

Related

Verify a function in PowerShell has run succesfully

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. :)

How to run embedded function

I am setting up a small script for a team of exchange admins at our MSP, the script consists of a 4 main functions and within these functions are more functions. I am having some trouble running the embedded functions. Below I have put an example of one of these functions "Manage-Teams"
I have added a Switch ($option) to see if this would resolve the issue, originally I had $option = Read-host -prompt "some text"
This did resolve the issue however I could not find it when tabbing through the functions
function Manage-Teams() {
Write-Host -ForegroundColor Yellow "What would you like to do? <Enable-AddGuests/Home>"
$option = Write-Host 'Would you like to allow or disable external access? Enable-AddGuests/Disable-AddGuest'
function Enable-AddGuests () {
#Set specific Group back to $True or $False
# GroupID is <Name.ExcterDirectoryObjectId>
$GroupID = get-unifiedgroup -Identity (Read-Host -prompt "object ID or SMTP") | Select-Object -ExpandProperty ExternalDirectoryObjectId
$SettingID = Get-AzureADObjectSetting -TargetType Groups -TargetObjectID $GroupID | select-object -expandproperty ID
remove-azureadobjectsetting -id $settingid -targettype Groups -TargetObjectID $GroupID
$template = Get-AzureADDirectorySettingTemplate | ? {$_.displayname -eq "group.unified.guest"}
$settingsCopy = $template.CreateDirectorySetting()
$settingsCopy["AllowToAddGuests"]= True
New-AzureADObjectSetting -TargetType Groups -TargetObjectId $groupID -DirectorySetting $settingsCopy
}
function Disable-AddGuests {
#Set specific Group back to $True or $False
# GroupID is <Name.ExcterDirectoryObjectId>
$GroupID = get-unifiedgroup -Identity (Read-Host -prompt "object ID or SMTP") | Select-Object -ExpandProperty ExternalDirectoryObjectId
$SettingID = Get-AzureADObjectSetting -TargetType Groups -TargetObjectID $GroupID | select-object -expandproperty ID
remove-azureadobjectsetting -id $settingid -targettype Groups -TargetObjectID $GroupID
$template = Get-AzureADDirectorySettingTemplate | ? {$_.displayname -eq "group.unified.guest"}
$settingsCopy = $template.CreateDirectorySetting()
$settingsCopy["AllowToAddGuests"]= False
New-AzureADObjectSetting -TargetType Groups -TargetObjectId $groupID -DirectorySetting $settingsCopy
}
Switch ($option)
{
Enable-AddGuests {Enable-AddGuests}
Disable-AddGuests {Disable-AddGuests}
Home {Home}
}
}
I am hoping for the following:
Manage-teams
"what would you like to do"
Enable-AddGuests
Runs function to enable guest access
Let me complement AdminOfThings' helpful answer by taking a step back:
If you want your nested functions to be seen outside the function they're defined in, simply define them directly in that outside scope.
By default, like variables, nested functions are local to the scope they're defined in and are also visible in descendant scopes, so that functions defined as siblings in the same scope can call each other.
In defining all your functions in the same scope, you avoid the awkwardness of using script: to define functions in a (fixed) different scope[1]:
While PowerShell allows you to modify other scopes, it's generally a bad idea from the perspective of robustness and maintainability.
By defining the script-level functions from inside another function, they do not become visible to the script scope until after the first call to the defining function.
Therefore, structure your code as follows:
# All functions are defined in the same scope, as siblings.
Function Enable-AddGuests {
# ...
}
Function Disable-AddGuests {
# ...
}
Function Manage-Teams {
$option = Read-Host "Would you like to allow or disable external access? Enable-AddGuests/Disable-AddGuests"
switch ($option) {
'Enable-AddGuests' { Enable-AddGuests; break }
'Disable-AddGuests' { Disable-AddGuests; break }
}
}
[1] Note that for code pasted or "dot-sourced" (from a script, using operator .) on the command line, the script: scope refers to the global scope.
This is a simplified version of your script for demonstration purposes.
Function Manage-Teams {
$option = Read-Host "Would you like to allow or disable external access? Enable-AddGuests/Disable-AddGuests"
Function script:Enable-AddGuests {
"Executing Enable-AddGuests"
}
Function script:Disable-AddGuests {
"Executing Disable-AddGuests"
}
Switch ($option) {
'Enable-AddGuests' {Enable-AddGuests}
'Disable-AddGuests' {Disable-AddGuests}
Default {"Entered an incorrect option"}
}
}
Output:
Manage-Teams
Would you like to allow or disable external access? Enable-AddGuests/Disable-AddGuests: Enable-AddGuests
Executing Enable-AddGuests
Manage-Teams
Would you like to allow or disable external access? Enable-AddGuests/Disable-AddGuests: Disable-AddGuests
Executing Disable-AddGuests
Manage-Teams
Would you like to allow or disable external access? Enable-AddGuests/Disable-AddGuests: HelpMe
Entered an incorrect option
Get-Help Enable-AddGuests
NAME
Enable-AddGuests
SYNTAX
Enable-AddGuests
ALIASES
None
REMARKS
None
Get-Help Disable-AddGuests
NAME
Disable-AddGuests
SYNTAX
Disable-AddGuests
ALIASES
None
REMARKS
None
Explanation:
I changed $option to use Read-Host to prompt the executor with a message and then store the typed in response. I scoped Enable-AddGuests and Disable-AddGuests to the script scope. I added the Default condition of your Switch statement to do something when you do not receive the values you are expecting at the prompt.
Once Manage-Teams is executed, you can then gain access to the Enable-AddGuests and Disable-AddGuests functions in this example because they are scoped to the script scope. By default, those functions would be local to their enclosing scope only, i.e. inside of Manage-Teams, and not visible to the outside. You will be able to tab complete them as well. If you want access to those functions without running Manage-Teams first, you will need to define and load them outside of Manage-Teams.
Seems like you are having a typo in your code.
You are using Write-Host cmdlet instead of Read-Host cmdlet.
Change this:
$option = Write-Host 'Would you like to allow or disable external access? Enable-AddGuests/Disable-AddGuest'
To this:
$option = Read-Host 'Would you like to allow or disable external access? Enable-AddGuests/Disable-AddGuest'

Get PowerShell function to write a table to the screen during a run but not append as a return

Here is a short example (my actual code requires me to output many more tables during the function run, and get a single returned output from it):
Function Select-RowFromCSV ($CSV)
{
$CSV
return $CSV[(read-host "select row # from CSV")]
}
Instead of outputting $CSV within the function it gets appended to the return and is getting into the variable that the function inserted to.
PS C:\Windows\system32> $ROW = Select-RowFromCSV -CSV (Import-Csv "C:\scripts\csv.csv")
select row # from CSV: 0
PS C:\Windows\system32> $ROW
Name Phone
Dan 111111
Dave 5555555
Oliver 666666
Dan 111111
PS C:\Windows\system32>
I tried multiple ways to try and print it to the screen, however unlike write-host that do work as expected for strings, none of the other one i tried works for non strings objects (FT, write-output, echo).
If you want to output something to the console without affecting the output of the Function, Write-Host is probably the simplest solution (but is considered harmful). Your code needs to be as follows:
Function Select-RowFromCSV ($CSV)
{
Write-Host ($CSV | Format-Table | Out-String)
Return $CSV[(read-host "select row # from CSV")]
}
$ROW = Select-RowFromCSV -CSV (Import-Csv "raid.csv")
As you observed, because your object isn't a string you need to Format it as you'd like and then convert it to a String (| Format-Table | Out-String).
However, you might want to consider using Write-Verbose which will write output to the Verbose stream instead, only when the -Verbose switch is used. To use this you need to add [cmdletbinding()] and a Param() block to your function, like this:
Function Select-RowFromCSV
{
[cmdletbinding()]
Param($CSV)
Write-Verbose ($CSV | Format-Table | Out-String)
Return $CSV[(read-host "select row # from CSV")-1]
}
Then execute your function with the -Verbose switch to see the extra output:
$ROW = Select-RowFromCSV -CSV (Import-Csv "raid.csv") -Verbose
Further Explanation:
In your original function you were outputting all of $CSV because it appeared on a line by itself, then also returning a row of it. The Return keyword is a little misleading in PowerShell, it doesn't define what is only returned, it just triggers the Function to immediately end and anything that has been output will go in to the output stream.
You should also note that the row number your user enters needs to start from 0, because Array indexes start from 0.
If you wanted the first row to be 1 rather than 0, you might want to do this:
$CSV[(read-host "select row # from CSV")-1]
You could also drop the Return keyword entirely as it's not necessary. If you want to be more explicit, I personally favour Write-Output.
Just use the following method:
$counter = 0
$table = #()
XXX | foreach-object {
do something;
$table +=[pscustomobject]# {
XXXX
}
$table[$counter]
$counter++
}

Passing a [ref] parameter in a remote session in Powershell

I have a Powershell question.
I am trying to get a value from a function to a variable, by calling a function with a reference to the variable.
For example:
$var = New.Object System.Object;
Example-Function -OutObject ([ref]$var);
Where the Example-Function is defined like this:
function Example-Function
{
[CmdletBinding()]
param
(
[Parameter(Mandatory=$true)]
[ref]
$OutObject
)
$SomeValue = ...
#Write some output
#Do something...
$OutObject.Value = $SomeValue;
}
This is working OK. The $var variable gets it's value from the function ($SomeValue).
But, this is not working when the Example-Function is imported into remote session, for example:
$creds = New-Object System.Management.Automation.PSCredential('user','pass')
$session = New-PSSession -ComputerName 'ExampleComputer' -Credential $creds -Authentication CredSSP
Import-PSSession -Session $session -CommandName 'Example-Function' -AllowClobber
$var = New.Object System.Object;
Example-Function -OutObject ([ref]$var);
This code is throwing the following error: Cannot process argument transformation on parameter 'OutObject'. Reference type is expected in argument.
I am assuming that this is becuase the Example-Function is now running on the other computer ('ExampleComputer'), while ([ref]$var) is referencing the variable in memory of the computer running the scripts (my computer).
The reason I don't want to (cannot) use the return statement way is becuase my function is writing some output, and in Powershell, everything that is outputed from a function is returned.
So, my question is, can I get a value from a function that has a lot of output into the variable, when the function is running in the remote session?
If it cannot be done by using the [ref] parameter, is there another way?
Thanks
Okay lets try again:
Invoke-Command returns whatever is run in the remote pipeline. Which means you can do:
$var = Invoke-Command -session $session -command {Example-Function}
Which saves everything in the $var variable. You can then filter the results and get whatever information you need.
And please remember [ref] just makes everything more complicated than it actually is.

Function parameters order in PowerShell

I have this code in one of my PowerShell scripts:
function callCommandWithArguments([String] $arg1, [String] $arg2)
{
[string]$pathToCommand = "C:\command.exe";
[Array]$arguments = "anArg", "-other", "$arg2", "$arg1";
# the real code is
# & $pathToCommand $arguments;
# but was not working, so I change it to debug
Write-Host $pathToCommand $arguments;
}
callCommandWithArguments("1", "2");
As the arguments order is changed in the $arguments array, I would expect this output:
C:\command.exe anArg -other 2 1
But instead I receive a strange:
C:\command.exe anArg -other 1 2
Am I missing something obvious?
try call your function like this:
callCommandWithArguments "1" "2"
In powershell you pass arguments to function without () and just separated by space.
In your code you are passsing a single argument array of type object[]