How to run embedded function - 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'

Related

Using Powershell introspection to find the name of a function passed as a parameter?

Say I'm passing a function as a parameter, is there a way to find out the name of the passed function through introspection in powershell? Or do I just have to pass it along with the rest of the parameters?
(without calling the function in question)
The linked question tries to pass a function by name, as a string, in which case the answer is obvious: the argument itself is the function name.
In case a script block is passed instead, you can use the following technique:
function get-ScriptBlockCommandName {
param(
[scriptblock] $ScriptBlock,
[switch] $Expand
)
# Using the script block's AST, extract the first command name / path token.
$commandName = $ScriptBlock.Ast.EndBlock.
Statements[0].PipelineElements.CommandElements[0].Extent.Text
# Expand (interpolate) the raw name, if requested.
if ($Expand) {
$commandName = $ExecutionContext.InvokeCommand.ExpandString($commandName)
}
# Remove outer quoting, if present.
if ($commandName -match '^([''"])(.+)\1$') {
$commandName = $Matches[2]
if ($Matches[1] -eq "'") { $commandName = $commandName -replace "''", "'" }
}
# Output
$commandName
}
The function returns the (first) command name / path that is called from inside the script block.
Caveats:
An error will occur if you pass an expression (e.g., 1 + 2) as the first statement inside the script block.
Only the first command is analyzed (and its command name / path returned), whereas there is no limit to how many statements you can place inside a script block.
By default, if the command name / path is constructed from variables / other commands, these are not expanded (interpolated), given that doing so can result in execution of commands; to opt into expansion, use the -Expand switch.
Example calls:
PS> get-ScriptBlockCommandName { foo -bar baz -more stuff }
foo
This also works with quoted names / paths (note how & must then be used to invoke the command):
PS> get-ScriptBlockCommandName { & '/dir name/foo' -bar baz -more stuff }
/dir name/foo
However, to avoid potentially unwanted execution of commands, the command name / path is returned as-is, with variable references and subexpressions unexpanded.
You can opt to have these expanded by passing -Expand:
PS> get-ScriptBlockCommandName { & "$HOME/scripts/foo.ps1" -bar baz } -Expand
C:/Users/jdoe/scripts.ps1 # e.g.

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

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

Powershell: How to throw an error if a CSV entry is blank

I've written an extensive script that runs through an AD termination process, and the script can obtain the necessary information from a CSV. How do I make it so that it errors out if the entry is blank in the CSV? I've tried putting in Try-Catch, If-Else, everything that I know how to do. I've tried changing the error action, and I can get it to throw system generated errors (ex. "Cannot bind parameter "Identity" to the target..."), but I cannot get it to do what I want. Please see the code example below:
(Yes, I know that I'm duplicating values. This of importance later on in the script, and not the part I'm having issues with)
$owner = $user.'Network User ID'}
$loginID = $user.'Network User ID'
$Identity = Get-ADUser -Identity $owner -Properties Displayname |Select-Object -ExpandProperty Displayname
$manager = $user.'Provide Inbox Access To'
$NewOwner = $user.'Provide users email group ownership to'
$NewOwnerID = $User.'Provide users email group ownership To'
What I need it to do is throw an error if ANY entry in the CSV is blank, and terminate. The most promising idea that I tried was:
If ($Owner -eq $Null)
{
Write-Host "Invalid entry, the Network User ID field cannot be blank"
Write-Host "Press Enter to Exit..."
Exit
}
Else
{
#Do everything else
}
But even that still fails.
In summary, what I need to do is throw a custom terminating error if an entry in the CSV is blank.
Any help is greatly appreciated!
EDIT
If this helps, here is more of the real code...
$Confirmation = Read-Host "Please double check the information in the file. Are you sure you want to continue? (Y/N)"
If($Confirmation -eq "Y")
{
Write-Host "You have chosen to proceed. Processing Termination" -BackgroundColor DarkCyan
#Import file
$file = "C:\TerminateUsers.csv"
$data = Import-Csv $file
#Set disabled OU
$disabledOU = "OU=Users,OU=Disabled Accounts, OU=Corporate"
$colOutput = #()
foreach ($user in $data)
{
#Grab variables from CSV
$owner = $user.'Terminated Network User ID'}
$loginID = $user.'Terminated Network User ID'
#Displayname required for Outlook functions
$Identity = Get-ADUser -Identity $owner -Properties Displayname |Select-Object -ExpandProperty Displayname
$manager = $user.'Provide Inbox Access To'
$NewOwner = $user.'Provide users email group ownership to'
$NewOwnerID = $User.'Provide users email group ownership To'
If (Get-ADUser -LDAPFilter "(sAMAccountName=$loginID)")
{
$date = Get-Date -Format d
#Disable account, change description, disable dialin, remove group memberships
Set-ADUser -Identity $loginID -Enabled $false
Set-ADUser -Identity $loginID -Replace #{Description = "Terminated $date"}
Set-ADUser -Identity $loginID -Replace #{msNPAllowDialin = $False}
RemoveMemberships $loginID
This isn't all of it, but this is the part we're working with...
There's a number of issues you're going to run into here.
First, $Owner -eq $Null isn't going to do what you likely want to do. Mainly, the issue is that an empty string is not a null value. They're different. Instead, your test should be:
if ([string]::IsNullOrEmpty($owner)) { ... }
Or:
if ([string]::IsNullOrWhiteSpace($owner)) { ... }
This second one returns true if the string includes only tabs, spaces, or other whitespace characters, or is an empty string, or is null.
Second, to throw an exception, you need to use the throw keyword. See Get-Help about_Throw. For example:
if ([string]::IsNullOrWhiteSpace($owner)) {
throw "Owner is null or empty.";
}
If you have this embedded in a try block, you can catch the exception with the associated catch blocks. See Get-Help about_Try_Catch_Finally. You can also use Trap, I believe (See Get-Help about_Trap).
Finally, the default action when an error is encountered is controlled by the $ErrorActionPreference variable. That variable's default value is Continue, so error messages will be displayed but the script will continue executing as though no error happened at all. I'm not entirely sure how this works with manually thrown exceptions and try/catch blocks, but unless I know that I want my script to ignore errors, I start just about every script with:
$ErrorActionPreference = Stop;
See Get-Help about_Preference_Variables and Get-Help about_CommonParameters for more about this one.
Consider the following dataset. Note the null for Last_Name for one of the columns.
user_name first_name last_name
--------- ---------- ---------
lrivera0 Lawrence Rivera
tlawrence1 Theresa Lawrence
rboyd2 Roy
cperry3 Christine Perry
jmartin4 Jessica Martin
So if we want to be sure to only process full rows then a simple If would cover that.
Import-Csv .\text.csv | ForEach-Object{
If($_.Psobject.Properties.Value -contains ""){
# There is a null here somewhere
Throw "Null encountered. Stopping"
} else {
# process as normal
}
}
Problem is that Import-CSV treats nulls as zero length strings. I tried using -contains on just $_ but it did not work as $_ is not an array but an object with properties. So I used the object properties value to perform the comparison against.
Bacon brought up an interesting point in that this code would not account for whitespace only empty values.
We use throw so processing stops if a null is encountered. Using that if block you can do whatever action you want.

Create a function with optional call variables

Is there a way to create a parameter in a PowerShell function where you have to call it in order to have it considered?
An example given by commandlet (the bold being what I want to do):
Invoke-Command -computername Server01 -Scriptblock {...}
Here is an example of what I want to do with the function
Function DoStuff($computername, -arg2, -domain $domain)
Test-parameter(-domain) if (-domain -eq $true) {
use $domain
}
Else {
$domain = "Domain1"
}
test-parameter($arg2) {
if ($arg2 -eq $true) {
Do something
}
else {
Do the opposite
}
}
So in summary:
If "-arg2" is present, I want something to happen in the script. If "-Domain" is present and has an argument with it, I want that to be used rather then the set argument.
Powershell provides a lot of built-in support for common parameter scenarios, including mandatory parameters, optional parameters, "switch" (aka flag) parameters, and "parameter sets."
By default, all parameters are optional. The most basic approach is to simply check each one for $null, then implement whatever logic you want from there. This is basically what you have already shown in your sample code.
If you want to learn about all of the special support that Powershell can give you, check out these links:
about_Functions
about_Functions_Advanced
about_Functions_Advanced_Parameters
I don't think your question is very clear, this code assumes that if you're going to include the -domain parameter, it's always 'named' (i.e. dostuff computername arg2 -domain domain); this also makes the computername parameter mandatory.
Function DoStuff(){
param(
[Parameter(Mandatory=$true)][string]$computername,
[Parameter(Mandatory=$false)][string]$arg2,
[Parameter(Mandatory=$false)][string]$domain
)
if(!($domain)){
$domain = 'domain1'
}
write-host $domain
if($arg2){
write-host "arg2 present... executing script block"
}
else{
write-host "arg2 missing... exiting or whatever"
}
}
Not sure I understand the question correctly.
From what I gather, you want to be able to assign a value to Domain if it is null and also what to check if $args2 is supplied and according to the value, execute a certain code?
I changed the code to reassemble the assumptions made above.
Function DoStuff($computername, $arg2, $domain)
{
if($domain -ne $null)
{
$domain = "Domain1"
}
if($arg2 -eq $null)
{
}
else
{
}
}
DoStuff -computername "Test" -arg2 "" -domain "Domain2"
DoStuff -computername "Test" -arg2 "Test" -domain ""
DoStuff -computername "Test" -domain "Domain2"
DoStuff -computername "Test" -arg2 "Domain2"
Did that help?

Adding PowerShell functions to an object

I have a PowerShell module with a group of functions.
The function createService creates an instance of a service and returns a variable. Several of my functions use the returned value, but I only want one instance of the service so I cannot call createService in each function.
On the command line, I can do $var = createService($string), then call update($var) and it will work properly, but I don't want to force the user to remember to use $var as a parameter.
Is there a way to put these functions in an object/class so the variable can be stored globally and referenced inside each function instead of through parameters?
I would propose to start the service by the exposed functions, so that a user does even have to care of starting it.
$module = {
# The only service instance, $null so far
$script:service = $null
# Starts the service once and keeps its the only instance
function Start-MyService {
if ($null -eq $script:service) {
"Starting service"
$script:service = 'MyService'
}
}
# Ensures the service by Start-MyService and then operates on $script:service
function Update-MyService1 {
Start-MyService
"Updating service 1: $script:service"
}
# Ensures the service by Start-MyService and then operates on $script:service
function Update-MyService2 {
Start-MyService
"Updating service 2: $script:service"
}
Export-ModuleMember -Function Update-MyService1, Update-MyService2
}
$null = New-Module $module
# Starting service
# Updating service 1: MyService
Update-MyService1
# Updating service 2: MyService
Update-MyService2
In your module, if you assign the service object to a script scoped variable, all functions in the module can access the variable. Here is an example:
$module = {
function StartNewService {
$script:service = 'MyService'
}
function UpdateService {
"Updating service: " + $script:service
}
Export-ModuleMember -Function StartNewService, UpdateService
}
$null = New-Module $module
# StartNewService creates the service variable.
StartNewService
# UpdateService accesses the service variable created by StartNewService.
UpdateService
If you declare the variable as $global:service, you can access the variable from outside the module as well.
Edit: To address the comments below, here is a more practical example that shows an appropriate situation for sharing a variable among functions in a module. In this case all of the functions in the module depend on the same instance of the $Locations variable. In this example the variable is created outside of the functions, and is kept private by not including it in the Export-ModuleMember command.
Here is a simplified version of my LocationName.psm1
$Locations = #{}
function Save-LocationName {
param(
[parameter(Mandatory=$true)]
[string]$Name
)
$Locations[$Name] = $PWD
}
function Move-LocationName {
param(
[parameter(Mandatory=$true)]
[string]$Name
)
if($Locations[$Name]) {
Set-Location $Locations[$Name]
}
else {
throw ("Location $Name does not exist.")
}
}
New-Alias -Name svln -Value Save-LocationName
New-Alias -Name mvln -Value Move-LocationName
Export-ModuleMember -Function Save-LocationName, Move-LocationName -Alias svln, mvln
With this module a user can give a name to a directory, and move to that location by using the given name. For example if I am at \\server01\c$\Program Files\Publisher\Application\Logs, I can save the location by entering svln logs1. Now if I change my location, I can return to the logs directory with mvln logs1. In this example it would be impractical to use the locations hashtable for input and output since the functions are always working with the same instance.