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
YorSubs
  • 3,194
  • 7
  • 37
  • 60
  • 1
    Does this answer your question ? https://stackoverflow.com/a/72105651/15339544 – Santiago Squarzon Sep 27 '22 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"`` – Santiago Squarzon Sep 27 '22 at 20:44
  • Thanks. So I used `Set-Variable outShortcuts -Value "$outShortcuts``nApp added: $AppName" -Scope Script` and that worked, so I guess that every action on a variable within the scope of the function is discarded when the function ends. It seems intuitive to me that a variable not referenced by the function itself would be regarded as global, but this is good to know (though I'll probably forget this in 6 months and hit the problem again!). Thanks. – YorSubs Sep 27 '22 at 20:46
  • 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) { ... }`. – zett42 Sep 27 '22 at 20:48
  • I sort of see what you mean, though it kind of messes up the structure of my functions by introducing another variable just to collect basic logging information. I'm sure you are right, I'll have work out what it means though (but the Set-Variable also works well for my needs!) – YorSubs Sep 27 '22 at 20:56
  • 2
    To paraphrase @mklement0 from this answer https://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... – mclayton Sep 27 '22 at 21:09
  • Ah yes, I see what you mean, that makes sense. I think I can remember it in this way. Thanks. – YorSubs Sep 27 '22 at 21:39
  • 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) – Santiago Squarzon Sep 27 '22 at 21:49

1 Answers1

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.

mclayton
  • 8,025
  • 2
  • 21
  • 26