0

I'm a bit confused about something in PowerShell.

At the end of a function it appends some text to a variable so that I can log what happened. But when I do this, $x just contains the header and nothing else. What am I doing wrong?

$x = "Header`n=========="

function xxx ($z) {
    $z = "$z ... 1"
    $x += "`noutput from $z"
}

xxx 123
xxx 234
xxx 345
xxx 456
8
  • 1
    Does this answer your question ? stackoverflow.com/a/72105651/15339544 Commented Sep 27, 2022 at 20:35
  • 2
    Also for this example, why would you want to update a variable outside the function's scope? seems like it would be better to just output the $x concatenated with the string in the function so, $x + "`noutput from $z" Commented Sep 27, 2022 at 20:44
  • 2
    I second what @SantiagoSquarzon wrote. Even cleaner, make $x a parameter too, so the function is "encapsulated" and doesn't have hidden dependencies, which can be hard to debug: function xxx ($x, $z) { ... }. Commented Sep 27, 2022 at 20:48
  • 2
    To paraphrase @mklement0 from this answer stackoverflow.com/a/38675054/3156906 - "while you can see (read) variables from ancestral (higher-up) scopes, assigning to a variable defined in an ancestral scope implicitly creates a new variable with the same name in the current scope." - that is, any assignments you make inside the function are applied to the distinct variable in the child scope (albeit with the same name as the variable in the parent scope), which is why you "lose" them when the function exits because you revert to the original variable in the parent scope... Commented Sep 27, 2022 at 21:09
  • 1
    It is often not the best option to have a function that "updates" something outside of it's scope, tho it can be useful on some niche cases. You should consider if in this case is not better to have your function return a new value that you can use later instead of updating an existing one. This is also something harder to debug (think about it) Commented Sep 27, 2022 at 21:49

1 Answer 1

1

To summarise the comments, and lean on this answer by @mklement0 from a similar question - https://stackoverflow.com/a/38675054/3156906 - the default behaviour is for PowerShell to let you read variables from a parent scope, but if you assign a value it creates a new variable in the child scope which "hides" the parent variable until the child scope exists.

The about_Scopes documentation says something similar as well, but doesn't really lay it out in such specific detail unfortunately:

If you create an item in a scope, and the item shares its name with an item in a different scope, the original item might be hidden under the new item, but it is not overridden or changed.

You can assign values to the variable in the parent scope if you refer to it explicitly by scope number - e.g:

$x = "some value"

function Invoke-MyFunction
{
    # "-Scope 1" means "immediate parent scope"
    Set-Variable x -Value "another value" -Scope 1
}

Invoke-MyFunction

write-host $x

In your case though, you might want to wrap your logging logic into separate functions rather than litter your code with lots of Set-Variable x -Value ($x + "another log line") -Scope 1 (which is an implementation detail of your logging approach).

Your current approach will degrade performance over time by creating a new (and increasingly long) string object every time you add some logging output, and it will also make it really hard to change your logging mechanism at a later date (e.g. what if you decide want to log to a file instead?) as you'll need to go back to every logging line and rewrite it to use the new mechanism.

What you could do instead is something like this:

Logging Code

$script:LogEntries = new-object System.Collections.ArrayList; 

function Write-LogEntry
{
    param( [string] $Value )
    $null = $script:LogEntries.Add($Value)
}

function Get-LogEntries
{
    return $script:LogEntries
}

Application Code

function Invoke-MyFunction
{
    Write-LogEntry -Value "another logging line"
}

and then your logging mechanism is abstracted away from your main application, and your application just has to call Write-LogEntry.

Note that $script:<varname> is another way of referencing variables in the containing script's root scope using Scope Modifiers - the link describes some other options including global, local and private.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.