Search code examples
powershellscopepowershell-2.0powershell-3.0powershell-remoting

Cannot modify a script-scoped variable from inside a function


I am currently making a script which is supposed to connect to 42 different local servers and getting the Users of a specific group (fjärrskrivbordsanvändare(Remote desktop users in swedish :D)) from active directory. After it has gotten all the users from the server it has to export the users to a file on MY desktop

The csv file has to look like this:

Company;Users
LawyerSweden;Mike
LawyerSweden;Jennifer
Stockholm Candymakers;Pedro
(Examples) 
etc.

Here's the code as of now:

cls

$MolnGroup = 'fjärrskrivbordsanvändare' 
$ActiveDirectory = 'activedirectory' 
$script:CloudArray
Set-Variable -Name OutputAnvandare -Value ($null) -Scope Script
Set-Variable -Name OutputDomain -Value ($null) -Scope Script

function ReadInfo {

Write-Host("A")

Get-Variable -Exclude PWD,*Preference | Remove-Variable -EA 0

if (Test-Path "C:\file\frickin\path.txt") {

    Write-Host("File found")

}else {

    Write-Host("Error: File not found, filepath might be invalid.")
    Exit
}


$filename = "C:\File\Freakin'\path\super.txt"
$Headers = "IPAddress", "Username", "Password", "Cloud"

$Importedcsv = Import-csv $filename -Delimiter ";" -Header $Headers

$PasswordsArray += @($Importedcsv.password)
$AddressArray = @($Importedcsv | ForEach-Object { $_.IPAddress } )
$UsernamesArray += @($Importedcsv.username)
$CloudArray += @($Importedcsv.cloud)

GetData
} 

function GetData([int]$p) {

Write-Host("B") 

for ($row = 1; $row -le $UsernamesArray.Length; $row++) 
{
    # (If the customer has cloud-service on server, proceed) 
    if($CloudArray[$row] -eq 1)
    {

        # Code below uses the information read in from a file to connect pc to server(s)

        $secstr = New-Object -TypeName System.Security.SecureString 
        $PasswordsArray[$row].ToCharArray() | ForEach-Object {$secstr.AppendChar($_)}
        $cred = new-object -typename System.Management.Automation.PSCredential -argumentlist $UsernamesArray[$row], $secstr

        # Runs command on server

        $OutputAnvandare = Invoke-Command -computername $AddressArray[$row] -credential $cred -ScriptBlock {

            Import-Module Activedirectory                

            foreach ($Anvandare in (Get-ADGroupMember fjärrskrivbordsanvändare)) 
            {

                $Anvandare.Name
            }
        }

        $OutputDomain = Invoke-Command -computername $AddressArray[$row] -credential $cred -ScriptBlock {

        Import-Module Activedirectory                

            foreach ($Anvandare in (Get-ADGroupMember fjärrskrivbordsanvändare)) 
            {

                gc env:UserDomain
            }
        }

    $OutputDomain + $OutputAnvandare
    }
  }
}


function Export {

Write-Host("C")

# Variabler för att bygga up en CSV-fil genom Out-File
$filsökväg = "C:\my\file\path\Coolkids.csv"
$ColForetag = "Company"
$ColAnvandare = "Users"
$Emptyline = "`n"
$delimiter = ";"

for ($p = 1; $p -le $AA.Length; $p++) {

    # writes out columns in the csv file
    $ColForetag + $delimiter + $ColAnvandare | Out-File $filsökväg

    # Writes out the domain name and the users
    $OutputDomain + $delimiter + $OutputAnvandare | Out-File $filsökväg -Append

    }
}

ReadInfo
Export

My problem is, I can't export the users or the domain. As you can see i tried to make the variables global to the whole script, but $outputanvandare and $outputdomain only contains the information i need inside of the foreach loop. If I try to print them out anywhere else, they're empty?!


Solution

  • This answer focuses on variable scoping, because it is the immediate cause of the problem.
    However, it is worth mentioning that modifying variables across scopes is best avoided to begin with; instead, pass values via the success stream (or, less typically, via by-reference variables and parameters ([ref]).

    To expound on PetSerAl's helpful comment on the question: The perhaps counter-intuitive thing about PowerShell variable scoping is that:

    • while you can see (read) variables from ancestral (higher-up) scopes (such as the parent scope) by referring to them by their mere name (e.g., $OutputDomain),

    • you cannot modify them by name only - to modify them you must explicitly refer to the scope that they were defined in.

    Without scope qualification, assigning to a variable defined in an ancestral scope implicitly creates a new variable with the same name in the current scope.

    Example that demonstrates the issue:

      # Create empty script-level var.
      Set-Variable -Scope Script -Name OutputDomain -Value 'original'
      # This is the same as:
      #   $script:OutputDomain = 'original'
    
      # Declare a function that reads and modifies $OutputDomain
      function func {
    
        # $OutputDomain from the script scope can be READ
        # without scope qualification:        
        $OutputDomain  # -> 'original'
    
        # Try to modify $OutputDomain.
        # !! Because $OutputDomain is ASSIGNED TO WITHOUT SCOPE QUALIFICATION
        # !! a NEW variable in the scope of the FUNCTION is created, and that
        # !! new variable goes out of scope when the function returns.
        # !! The SCRIPT-LEVEL $OutputDomain is left UNTOUCHED.
        $OutputDomain = 'new'
    
        # !! Now that a local variable has been created, $OutputDomain refers to the LOCAL one.
        # !! Without scope qualification, you cannot see the script-level variable
        # !! anymore.
        $OutputDomain  # -> 'new'
      }
    
      # Invoke the function.
      func
    
      # Print the now current value of $OutputDomain at the script level:
      $OutputDomain # !! -> 'original', because the script-level variable was never modified.
    

    Solution:

    There are several ways to add scope qualification to a variable reference:

    • Use a scope modifier, such as script in $script:OutputDomain.

      • In the case at hand, this is the simplest solution:
        $script:OutputDomain = 'new'

      • Note that this only works with absolute scopes global, script, and local (the default).

      • A caveat re global variables: they are session-global, so a script assigning to a global variable could inadvertently modify a preexisting global variable, and, conversely, global variables created inside a script continue to exist after the script terminates.

    • Use Get/Set-Variable -Scope, which - in addition to supporting the absolute scope modifiers - supports relative scope references by 0-based index, where 0 represents the current scope, 1 the parent scope, and so on.

      • In the case at hand, since the script scope is the next higher scope,
        Get-Variable -Scope 1 OutputDomain is the same as $script:OutputDomain, and
        Set-Variable -Scope 1 OutputDomain 'new' equals $script:OutputDomain = 'new'.
    • (A rarely used alternative available inside functions and trap handlers is to use [ref], which allows modifying the variable in the most immediate ancestral scope in which it is defined: ([ref] $OutputDomain).Value = 'new', which, as PetSerAl points out in a comment, is the same as (Get-Variable OutputDomain).Value = 'new')

    For more information, see:


    Finally, for the sake of completeness, Set-Variable -Option AllScope is a way to avoid having to use scope qualification at all (in all descendent scopes), because effectively then only a single variable by that name exists, which can be read and modified without scope qualification from any (descendent) scope.

      # By defining $OutputDomain this way, all descendent scopes
      # can both read and assign to $OutpuDomain without scope qualification
      # (because the variable is effectively a singleton).
      Set-Variable -Scope Script -Option AllScope -Name OutputDomain
    

    However, I would not recommend it (at least not without adopting a naming convention), as it obscures the distinction between modifying local variables and all-scope variables:
    in the absence of scope qualification, looking at a statement such as $OutputDomain = 'new' in isolation, you cannot tell if a local or an all-scope variable is being modified.