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.
Related
I would like to have a function to run different ScriptBlocks. So, I need to use my Scriptblock as the parameter of the function. It does not work.
For example. This function returns the ScriptBlock as a string.
function Run_Scriptblock($SB) {
return $SB
}
These are the outputs from my tries:
# 1st try
Run_Scriptblock {systeminfo}
>> systeminfo
# 2nd try
Run_Scriptblock systeminfo
>> systeminfo
# 3rd try
Run_Scriptblock [scriptblock]systeminfo
>> [scriptblock]systeminfo
# 4th try
$Command = [scriptblock]{systeminfo}
Run_Scriptblock $Command
>> [scriptblock]systeminfo
# 5th try
[scriptblock]$Command = {systeminfo}
Run_Scriptblock $Command
>> systeminfo
If you want a function to run a scriptblock, you need to actually invoke or call that scriptblock, i.e.
function Run_Scriptblock($SB) {
$SB.Invoke()
}
or
function Run_Scriptblock($SB) {
& $SB
}
Otherwise the function will just return the scriptblock definition in string form. The return keyword is not needed, since PowerShell functions return all non-captured output by default.
The function would be called like this:
Run_Scriptblock {systeminfo}
As a side note, I would recommend you consider naming your function following PowerShell conventions (<Verb>-<Noun> with an approved verb), e.g.
function Invoke-Scriptblock($SB) {
...
}
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'
I want access a function inside filesystemwatcher created event function. I tried using a global function but i never see output on console.
#Script Parameters
param(
[Parameter(Mandatory=$True, position=1)]
[String]$path
)
#Global Function
function global:myFunction (){
Write-Host "myFunction"
}
#FileSystemWatcher properties
$fsw = New-Object System.IO.FileSystemWatcher
$fsw.Path = $path
$fsw.Filter = ""
$fsw.IncludeSubDirectories = $True
$fsw.EnableRaisingEvents = $True
#Created event function
Register-ObjectEvent -InputObject $fsw -EventName Created -Action{
$global:myFunction #trying to access global function
}
Your only problem is a syntax confusion about how to invoke a global function:
$global:myFunction # WRONG - looks for *variable*
looks for a variable named myFunction in the global scope.
Omit the $ to call the function:
global:myFunction # OK - calls function
That said, given that all scopes in a given session by default see global definitions, you don't need the global: scope specifier - simply invoke myFunction:
The only time you need global: explicitly is if there's a different myFunction definition in the current scope or in an ancestral scope, and you want to explicitly target the global definition.
Without global:, such a different definition would shadow (hide) the global definition.
To put it all together:
# Script Parameters
param(
[Parameter(Mandatory=$True, position=1)]
[String]$path
)
# Global Function
function global:myFunction {
param($FullName)
Write-Host "File created: $FullName"
}
# FileSystemWatcher properties
$fsw = New-Object System.IO.FileSystemWatcher
$fsw.Path = $path
$fsw.Filter = ""
$fsw.IncludeSubDirectories = $True
$fsw.EnableRaisingEvents = $True
# Created-event function
$eventJob = Register-ObjectEvent -InputObject $fsw -EventName Created -Action {
myFunction $EventArgs.FullPath # Call the global function.
}
Note that I've extended the code to pass the full filename of the newly created file to myFunction, via the automatic $EventArgs variable.
Alternatives:
Modifying the global scope from a script can be problematic due to the potential for name collisions, not least because global definitions linger even after the script terminates.
Therefore, consider:
either: moving the code of function myFunction directly into the -Action script block.
or: calling a (possibly temporary) script file from the -Action script block.
Also note that event action blocks typically write output to the success output stream, not directly to the host with Write-Host - if they need to produce output at all - where it can be collected on demand via the Receive-Job cmdlet.
You could specify explicitly your function inside Action scriptblock. Like so
#FileSystemWatcher properties
$fsw = New-Object System.IO.FileSystemWatcher
$fsw.Path = $path
$fsw.Filter = ""
$fsw.IncludeSubDirectories = $True
$fsw.EnableRaisingEvents = $True
#Created event function
Register-ObjectEvent -InputObject $fsw -EventName Created -Action {
#Describe your function here
function global:myFunction (){
Write-Host "myFunction"
}
#There call your function
global:myFunction
}
First time in PowerShell 5 and I'm having trouble calling a function that writes messages to a file from another function. The following is a simplified version of what I'm doing.
workflow test {
function logMessage {
param([string] $Msg)
Write-Output $Msg
}
function RemoveMachineFromCollection{
param([string]$Collection, [string]$Machine)
# If there's an error
LogMessage "Error Removing Machine"
# If all is good
LogMessage "successfully remove machine"
}
$Collections = DatabaseQuery1
foreach -parallel($coll in $Collections) {
logMessage "operating on $coll collection"
$Machines = DatabaseQuery2
foreach($Mach in $Machines) {
logMessage "Removing $Mach from $coll"
RemoveMachineFromCollection -Collection $coll -Machine $Mach
}
}
}
test
Here's the error it generates:
The term 'logMessage' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
+ CategoryInfo : ObjectNotFound: (logMessage:String) [], CommandNotFoundException
+ FullyQualifiedErrorId : CommandNotFoundException
+ PSComputerName : [localhost]
I've tried moving the logMessage function around in the file and even tried Global scope.
In any other language I would be able to call logMessage from any other function. As that's the purpose of a function.
What's the "Workflow way" of reusing a block of code?
Do I need to create some logging module that gets loaded into the Workflow?
You could move the functions and function call to an InlineScript (PowerShell ScriptBlock) inside the workflow like below.
workflow test {
InlineScript
{
function func1{
Write-Output "Func 1"
logMessage
}
function logMessage{
Write-Output "logMessage"
}
func1
}
}
Would Output:
Func 1
logMessage
As #JeffZeitlin mentioned in his answer, workflows are not PowerShell and are much more restrictive. The InlineScript block allows for normal PowerShell code to be interpreted however the scope will be tied to the InlineScript block. For instance, if you define the functions in the script block then attempt to call the func1 function outside of the InlineScript block (but still within the workflow) it will fail because it is out of scope.
The same would happen if you define the two functions either outside of the workflow or inside of the workflow but not in an InlineScript block.
Now for an example of how you can apply this to running a foreach -parallel loop.
workflow test {
## workflow parameter
param($MyList)
## parallel foreach loop on workflow parameter
foreach -parallel ($Item in $MyList)
{
## inlinescript
inlinescript
{
## function func1 declaration
function func1{
param($MyItem)
Write-Output ('Func 1, MyItem {0}' -f $MyItem)
logMessage $MyItem
}
## function logMessage declaration
function logMessage{
param($MyItem)
Write-Output ('logMessage, MyItem: {0}' -f $MyItem)
}
## func1 call with $Using:Item statement
## $Using: prefix allows us to call items that are in the workflow scope but not in the inlinescript scope.
func1 $Using:Item
}
}
}
Example call to this workflow would look like this
PS> $MyList = 1,2,3
PS> test $MyList
Func 1, MyItem 3
Func 1, MyItem 1
Func 1, MyItem 2
logMessage, MyItem: 3
logMessage, MyItem: 2
logMessage, MyItem: 1
You will notice (and as expected) the output order is random since it was run in parallel.
Powershell requires that functions be defined before use ('lexical scope'). In your example, you are calling the logMessage function before you have defined it.
You have also structured your example as a Powershell workflow. Workflows have some restrictions that ordinary scripts do not; you need to be aware of those differences. I did this search to find some descriptions and discussions of the differences; the first "hit" provides good information. I have not (yet) found anything saying whether functions can be defined in workflows, but I would be very wary of defining functions within functions (or workflows) in the first place.
Your logMessage function is not visible from within func1 function. It's valid even though logMessage function is declared above func1 one.
For this simple case, you could use nested functions as follows:
workflow test {
function func1 {
function logMessage {
Write-Output "logMessage"
}
Write-Output "Func 1"
logMessage
}
func1
}
test
Output:
PS D:\PShell> D:\PShell\SO\41770877.ps1
Func 1
logMessage
I'm stuck trying to figure out the best method for calling a function from a function and making parameters mandatory for both functions. I've got the below so far, and things work because I know what cmdline params to specify.
I did find this post but I'm not sure how to use that with a function that calls a function.
Edit: added shorter code. In the code, How would you make the ParamSet parameter [string]$killserver mandatory for both the parent function main and the child function KillSwitch so that if the function is run main -nukejobs Powershell prompts for the variable $killserver
Edit 2: worked out the prompting for the mandatory param serverlist and datelist but it appears now the child function doesn't write to host "receive input from $serverlist and $datelist"
Edit 3: corrected the Switch ($PSCmdlet.ParameterSetName){ value for RunMulti and now things look good.
Function Main{
[CmdletBinding(SupportsShouldProcess=$true,DefaultParameterSetName="ViewOnly")]
Param(
[Parameter(Mandatory=$false,ParameterSetName="KillSwitch")]
[Switch]$NukeJobs,
[Parameter(Mandatory=$true,ParameterSetName="KillSwitch",
HelpMessage="Enter ServerName to remove the scheduled reboot for, Check using main -viewonly")]
[String]$killserver,
[Parameter(Mandatory=$false,ParameterSetName="RunMulti")]
[switch]$RunMultiple,
[Parameter(Mandatory=$true,ParameterSetName="RunMulti")]
[String]$serverlist,
[Parameter(Mandatory=$true,ParameterSetName="RunMulti")]
[String]$datelist
)
Switch ($PSCmdlet.ParameterSetName) {
"KillSwitch" {
KillSwitch -server $killserver
} # end killswitch
"RunMulti" {
RunMulti -serverlist $serverlist -datelist $datelist
} # end run multi
} # end switch block
} # end main function
Function KillSwitch{
Param(
[Parameter(Mandatory=$true)]
[String]$server
)
"Removing previous scheduled reboot for $server"
} # end killswitch function
Function RunMulti {
Param(
[Parameter(Mandatory=$true,
HelpMessage="Text file with server names to reboot, one per line!")]
[string]$serverlist,
[Parameter(Mandatory=$true,
HelpMessage="Text file with date/times, one per line!")]
[String]$datelist
)
"receive input from $serverlist and $datelist"
}
I found the mandatory parameters needed separate:
[Parameter(Mandatory=$true,ParameterSetName="RunOnce")] blocks.
For example, this won't work, even if Mandatory=$true
My guess is because it only applies to [switch]
[Parameter(Mandatory=$false,ParameterSetName="RunOnce",
HelpMessage="Enter ServerName to schedule reboot for.")]
[switch]$RunOnce,
[string]$server,
[string]$date,
But this will work, causing Powershell to prompt for $server and $date when the RunOnce switch is given.
[Parameter(Mandatory=$false,ParameterSetName="RunOnce",
HelpMessage="Enter ServerName to schedule reboot for.")]
[switch]$RunOnce,
[Parameter(Mandatory=$true,ParameterSetName="RunOnce")]
[string]$server,
[Parameter(Mandatory=$true,ParameterSetName="RunOnce")]
[string]$date,