1

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?!

Cœur
  • 37,241
  • 25
  • 195
  • 267
simpen
  • 134
  • 2
  • 18
  • 1
    `$OutputAnvandare = ...` that will create new variable in current scope, even though variable with the same name already exists in parent scope. `$script:OutputAnvandare = ...` – user4003407 Jul 29 '16 at 14:47
  • 1
    It's a good question, but to get to the heart of it, one must read through a lot of code; please consider providing an [MCVE (Minimal, Complete, and Verifiable Example)](http://stackoverflow.com/help/mcve) in the future. – mklement0 Jul 30 '16 at 13:39

2 Answers2

5

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.

Community
  • 1
  • 1
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • 1
    You could also use `(Get-Variable OutputDomain).Value = 'new'` which work the same way as `[ref]` thing. – user4003407 Jul 30 '16 at 14:47
  • 1
    Thank you both for the really good answers. I'm going to edit my code and see if it works. Just one question, do I have to set the value of the variable when I assign it to the whole script? Basically, do iI have to do this: `$script:outoutanvandare = "Value"` Or can I just do this `$script:outputdomain` – simpen Aug 01 '16 at 06:46
  • @tTim: No, you don't have to explicitly initialize the variable; simply assigning to it with the scope modifier will create it on demand (from any scope); try `function func { $script:foo = 'bar' }; func; $script:foo`. – mklement0 Aug 01 '16 at 07:01
3

Since you've mentioned that you want to learn, I hope you'll pardon my answer, which is a bit longer than normal.

The issue that's impacting you here is PowerShell Variable Scoping. When you're commiting the values of $outputAvandare and $outputDomain, they only exist for as long as that function is running.

Function variables last until the function ends.
Script variables last until the script ends.
Session/global variables last until the session ends.
Environmental variable persist forever.

If you want to get the values out of them, you could make them Global variables instead, using this syntax:

$global:OutputAnvandare = blahblahblah

While that would be the easiest fix for your code, Global variables are frowned upon in PowerShell, since they subvert the normal PowerShell expectations of variable scopes.

Much better solution :)

Don't be dismayed, you're actually almost there with a really good solution that conforms to PowerShell design rules.

Today, your GetData function grabs the values that we want, but it only emits them to the console. You can see this in this line on GetData:

 $OutputDomain + $OutputAnvandare

This is what we'd call emitting an object, or emiting data to the console. We need to STORE this data instead of just writing it. So instead of simply calling the function, as you do today, do this instead:

$Output = GetData

Then your function will run and grab all the AD Users, etc, and we'll grab the results and stuff them in $output. Then you can export the contents of $output later on.

Community
  • 1
  • 1
FoxDeploy
  • 12,569
  • 2
  • 33
  • 48
  • 2
    Good advice, but please suggest `$script:OutputAnvandare`, not `$global:OutputAnvandare` as the quick fix, because there's no need to create a _global_ variable, which would persist in the PowerShell session even after the script terminates and potentially modify a preexisting global variable by that name. – mklement0 Jul 29 '16 at 15:40
  • Sounds good, I modified my answer to instead focus on how he could avoid the whole Cross scopiing quagmire. – FoxDeploy Jul 31 '16 at 13:36
  • While I agree that avoidance of cross-scope variable modification is worth promoting, you not only neglect to acknowledge and address the OP's explicit attempt to modify _script_-scoped variables, you instead suggest the even more pernicious use of _global_ variables as a quick fix. – mklement0 Jul 31 '16 at 22:38
  • Hi! Thank you for the answer! I have one question for you though. Since, from what I understood, all the AD Users and the domain names are now saved in `$Output` Instead of `$OutputAnvandare` and `$OutputDomain`. So my question is, do I really need to keep `$OutputAnvandare = Invoke-Command -computername (...)` and `$OutputDomain = Invoke-Command -computername (...)`. Also, I declared the `$Output` as a script variable in the beginning of the code, then I call on the function like this `$Output = GetData`. But when i try to print it it still doesn't contain anything? – simpen Aug 01 '16 at 09:43
  • 1
    Make sure you have your function declarations at the top of the script. After looking at your code again, I noticed you call the function `GetData` before you define it. This practice, which works in other languages, doesn't work in PowerShell. – FoxDeploy Aug 01 '16 at 14:28