Unable to modify function argument in Powershell - function

I have a function that is similar to this:
function A-Function{
[CmdletBinding(SupportsShouldProcess=$True)]
param (
[Parameter(Position=0, HelpMessage="A Test string", Mandatory=$true)]
[string]$Path,
[Parameter(Position=1, HelpMessage="The list of file names to download.", ValueFromPipeline=$True)]
[string[]]$testVar,
[Parameter(HelpMessage= "The username")]
[string]$User,
[Parameter(HelpMessage= "The password")]
[string]$Password,
[Parameter(HelpMessage= "The credentials used.")]
[Net.NetworkCredential]$Credential = (New-Object Net.NetworkCredential("Anonymous", ""))
)
Begin {
$Path = "TEST_" + $Path
if ($User) {
if ($Password) {
$Credential = New-Object Net.NetworkCredential($User, $Password)
}
}
}
Process {
$Path
$Credential
}
End {
}
}
If I run "A-Function test -User test -Password -test", I get the output :
TEST_test
UserName Password SecurePassword Domain
-------- -------- -------------- ------
test test System.Security.SecureString
This is what I expected the output to be. However, if I run this command instead:
"Test" | A-Function test -User test -Password -test
I get this output instead:
TEST_test
UserName Password SecurePassword Domain
-------- -------- -------------- ------
Anonymous System.Security.SecureString
In other words, in the second scenario, it hasn't changed the value of the Credential argument in the Begin section, but in the first, it has. I don't understand why this is, can someone explain it?
Regards

If you do a
trace-command parameterbinding {"Test" | A-Function test -User test -Password test} -pshost
you will see that $credential, since it is not being passed as argument to the function, it is bound with the default value each time during process. Path, on the other hand, is bound only once since you pass it to the function and hence the change that you do in begin is available in process.
This is definitely a bug / something not really optimal, as this is not reproduced in Powershell v3. In v3, you get the desired output of credential being test rather than anonymous.
You can fix this for now, of course, by using a local variable in begin or scoping the existing ones with $script:Path etc:
function A-Function{
[CmdletBinding(SupportsShouldProcess=$True)]
param (
[Parameter(Position=0, HelpMessage="A Test string", Mandatory=$true)]
[string]$Path,
[Parameter(Position=1, HelpMessage="The list of file names to download.", ValueFromPipeline=$True)]
[string[]]$testVar,
[Parameter(HelpMessage= "The username")]
[string]$User,
[Parameter(HelpMessage= "The password")]
[string]$Password,
[Parameter(HelpMessage= "The credentials used.")]
[Net.NetworkCredential]$Credential = (New-Object Net.NetworkCredential("Anonymous", ""))
)
Begin {
$script:Path = "TEST_" + $Path
if ($User) {
if ($Password) {
$script:Credential = New-Object Net.NetworkCredential($User, $Password)
}
}
}
Process {
$script:Path
$script:Credential
}
End {
}
}
"Test" | A-Function test -User test -Password test

Yeah, I've been bitten by this. The function parameters get reset every iteration of the Process block. Just set a local variable in your Begin block and then use that local variable in the Process block instead of using the parameter variable.
Edit:
I have to confess that I didn't look very hard at your sample code as I immediately recognized a problem from the combination of the title of your question and the fact that you were setting $Path in the Begin block. Looking at your function a little harder, makes me wonder why you have a Process block when you don't process any pipeline input.
My recollection is that changes you make to the function parameters in the Begin block will survive the first iteration of the Process block. They will only get reset on subsequent iterations (or, perhaps, at the end of the first iteration).

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'

Calling a Function From Another Function in PowerShell

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

Reuse parameterized (prepared) SQL Query

i've coded an ActiveDirectory logging system a couple of years ago...
it never become a status greater than beta but its still in use...
i got an issue reported and found out what happening...
they are serveral filds in such an ActiveDirectory Event witch are UserInputs, so i've to validate them! -- of course i didnt...
so after the first user got the brilliant idea to use singlequotes in a specific foldername it crashed my scripts - easy injection possible...
so id like to make an update using prepared statements like im using in PHP and others.
Now this is a Powershell Script.. id like to do something like this:
$MySQL-OBJ.CommandText = "INSERT INTO `table-name` (i1,i2,i3) VALUES (#k1,#k2,#k3)"
$MySQL-OBJ.Parameters.AddWithValue("#k1","value 1")
$MySQL-OBJ.Parameters.AddWithValue("#k2","value 2")
$MySQL-OBJ.Parameters.AddWithValue("#k3","value 3")
$MySQL-OBJ.ExecuteNonQuery()
This would work fine - 1 times.
My Script runs endless as a Service and loops all within a while($true) loop.
Powershell clams about the param is already set...
Exception calling "AddWithValue" with "2" argument(s): "Parameter
'#k1' has already been defined."
how i can reset this "bind" without closing the database connection?
id like the leave the connection open because the script is faster without closing and opening the connections when a event is fired (10+ / sec)
Example Code
(shortend and not tested)
##start
function db_prepare(){
$MySqlConnection = New-Object MySql.Data.MySqlClient.MySqlConnection
$MySqlConnection.ConnectionString = "server=$MySQLServerName;user id=$Username;password=$Password;database=$MySQLDatenbankName;pooling=false"
$MySqlConnection.Open()
$MySqlCommand = New-Object MySql.Data.MySqlClient.MySqlCommand
$MySqlCommand.Connection = $MySqlConnection
$MySqlCommand.CommandText = "INSERT INTO `whatever` (col1,col2...) VALUES (#va1,#va2...)"
}
while($true){
if($MySqlConnection.State -eq 'closed'){ db_prepare() }
## do the event reading and data formating stuff
## bild some variables to set as sql param values
$MySQLCommand.Parameters.AddWithValue("#va1",$variable_for_1)
$MySQLCommand.Parameters.AddWithValue("#va2",$variable_for_2)
.
.
.
Try{ $MySqlCommand.ExecuteNonQuery() | Out-Null }
Catch{ <# error handling #> }
}
Change your logic so that the db_prepare() method initializes a MySql connection and a MySql command with parameters. Set the parameter values for pre-declared parameter names in loop. Like so,
function db_prepare(){
# ...
# Add named parameters
$MySQLCommand.Parameters.Add("#val1", <datatype>)
$MySQLCommand.Parameters.Add("#val2", <datatype>)
}
while($true) {
# ...
# Set values for the named parameters
$MySQLCommand.Parameters.SetParameter("#val1", <value>)
$MySQLCommand.Parameters.SetParameter("#val2", <value>)
$MySqlCommand.ExecuteNonQuery()
# ...
}

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.