powershell dot sourced function parameter overwrites local variables - powershell

I'm using powershell dot-sourcing to set variables in the current scope and have encountered an interesting feature.
It seems that the parameter of the function will also overwrite any local variable of the same name.
Is this expected?
Should I just use $global:MyVar instead to set variables in the local scope from other scripts?
# Given
function TestX([string]$X)
{
Write-Host "`$X = $X"
}
# And variable $X
# Note that the variable name is the same as the parameter name in 'TestX'
$X = "MyValue"
PS> TestX $X
MyValue
PS> $X; TestX "123456"
MyValue
123456
PS> $X; . TestX "123456"
MyValue
123456
PS> $X; . TestX "123456"
123456
123456
EDIT:
To expand on what I'm trying to accomplish...
I have a set of scripts used for a build process. These scripts are used to target multiple environments. There are different configurations for each environment (DEV, TEST, QA, PROD) that apply different rules/settings/etc. These configurations are stored in directories. Among these settings are some powershell files that are used to set script-wide settings for that particular environment. For example, target server URL, target server UNC, etc..
Among the build process scripts there is a function Confirm-TargetEnvironmentVariables. As the name implies, it checks to see if the environment variables have been loaded and if not, loads them. This function is sprinkled throughout the various script files/functions to ensure that when a function uses one of these script-wide variables, it has been set.
It was this function that I used to call with dot-sourcing.
function Confirm-TargetEnvironmentVariables([string]$TargetEnvironment)
{
...
}
# Like this..
. Confirm-TargetEnvironmentVariables "PROD"
This all worked just fine. Until the I had a need to switch between loading variables from more than 1 environment (for refreshing TEST from PROD for example, I need variable info from both). And in fact this still works, except for the fact that in the script that was calling Confirm-TargetEnvironmentVariables I already had a variable called $TargetEnvironment. So I was trying to do this:
$SourceEnvironment = "PROD"
$TargetEnvironment = "TEST"
. Confirm-TargetEnvironmentVariables $SourceEnvironment
# Do stuff with loaded "PROD" variables...
. Confirm-TargetEnvironmentVariables $TargetEnvironment
# Do stuff with loaded "TEST" variables...
But what was happening was this:
$SourceEnvironment = "PROD"
$TargetEnvironment = "TEST"
. Confirm-TargetEnvironmentVariables $SourceEnvironment
# Do stuff with loaded "PROD" variables...
# The value of $TargetEnvironment has been set to "PROD" by dot-sourcing!!
. Confirm-TargetEnvironmentVariables $TargetEnvironment
# Do stuff with loaded... "PROD" variables!!!
So this should provide more context hopefully. But ultimately it still raises the question of why dot-sourcing includes parameter variables when bringing variables into the local scope. Is this by design? I can't think of a scenario where this would be desired behavior though.

Should just use $global:MyVar instead to set variables in the local scope from other scripts?
I'd recommend you avoid writing functions that either requires dot-sourcing to work correctly, or that write to global variables.
Instead, use Set-Variable's -Scope parameter to write to a variable in the calling scope:
function Test-SetVariable
{
param([string]$Name,$Value)
# '1' means "one level up", so it updates the variable in the caller's scope
Set-Variable -Name $Name -Value $Value -Scope 1
}

Related

Accessing a hash table through a module

I have defined this in module as defblah.ps1
$defblah= #{
first = "aaa";
seconf = "bbb";
}
I also have this in module as Blah.ps1
Function Blah
{
Write-Host $defblah.first;
}
I did Export-ModuleMember and then Import-Module, so everything with module definition (export, import) is fine.
Then, after importing I run Blah, it prints "aaa", which is what i expected.
The problem is when I type in
Write-Host $defblah.first;
it returns nothing. How can I make my last statement work and return "aaa"?
Generally, I suggest not exporting variables from a module, even though it is technically supported.
A module's implicit export behavior indeed precludes variables - in PSv5+, only functions and aliases are exported by default - meaning the absence of an Export-ModuleMember and/or module manifest (*.psd1) controlling the exports.
The general expectation is for a module to export commands (functions / cmdlets and possibly also aliases), not data (variables) - and commands alone present enough potential for name collisions.
If you truly want to export variables from your module, use an Export-ModuleMember call with the
-Variable parameter and/or - if your module comes with a manifest file - export the variables via the VariablesToExport key.

Windows $env:path ="$($env:path);." where was it added?

I "fixed" a problem by running $env:path ="$($env:path);." from PowerShell. Apparently it added the current directory to my path. Which path variable did it add to please? In my environment variables dialogue, where would I see it added? User variables? System variables?
I'm confused because I had already added the folder to system variables path, but couldn't run the contained script until running ``$env:path ="$($env:path);."
Updates to $env:EnvVarName affect the current process only - no persistent changes via the registry are made:
$env:EnvVarName = 'foo'
is the equivalent of calling .NET method System.Environment.SetEnvironmentVariable as follows:
[Environment]::SetEnvironmentVariable('EnvVarName', 'foo', 'Process')
That is, the scope of the update is the current process.
Only if you substitute 'User' or 'Machine' for 'Process' in the above call (supported on Windows only[1]) do you persistently update environment variables in the registry (for the current user or the local machine (all users), respectively), for future sessions (processes)[2].
As of PowerShell [Core] 7.2, there is no PowerShell-native way to persistently update environment variables, but introducing one is being discussed on GitHub.
In other words: if you want to update not only the registry-based definition but also the value in the current process, you need to make both calls; e.g., for the current user:
# Windows only: Update / create a persistent definition for the current user,
# stored in the registry.
[Environment]::SetEnvironmentVariable('EnvVarName', 'foo', 'User')
# Update value for current process too.
$env:EnvVarName = 'foo'
Or, more in the spirit of DRY:
'User', 'Process' | foreach {
[Environment]::SetEnvironmentVariable('EnvVarName', 'foo', $_)
}
If the new value is to be based on the existing one from a given registry scope, retrieve the scope-specific value via System.Environment.GetEnvironmentVariable; e.g.:
# Get the registry-based *user* value
[Environment]::GetEnvironmentVariable('Path', 'User')
Caveat: Non-support for Windows environment variables based on REG_EXPAND_SZ registry values:
On Windows, persistently defined environment variables may be defined based on other environment variables, namely if the underlying registry value defining the variable is of type REG_EXPAND_SZ.
As of .NET 6, the System.Environment type's methods do not (directly) support such environment variables:
On getting such a variable's value, its expanded form is invariably returned; that is, references to other environment variables such as %SystemRoot% are replaced by their values.
On setting environment variables, REG_SZ registry values are invariably created, i.e. static, verbatim values - even when updating an existing REG_EXPAND_SZ value.
While quietly converting REG_EXPAND_SZ environment variables to static REG_SZ ones may often have no ill effects (as long as the new value only contains literal values), it certainly can: for instance, say a variable is defined in terms of %JAVADIR%; if that variable is converted to a static value based on the then-current value of %JAVADIR%, it will stop working if the value of %JAVADIR% is later changed.
Unfortunately, retrieval of raw REG_EXPAND_SZ environment variables and proper updating of their values currently requires direct registry access, which is quite cumbersome (not even the Windows API seems to have support for it) - see this answer.
Important considerations for the Path environment variable ($env:PATH) on Windows:
The Path environment variable is special in that it is a composite value: when a process starts, the in-process value is the concatenation of the Machine (local machine, for all users) value and the User (current user) value.
Note that since the machine-level value comes first, its entries take precedence over the user-level value's entries.
Therefore, if you want to modify (append to) the existing Path, it's better not to define the new value simply by appending to the existing in-process value ($env:Path), because you'll be duplicating the Machine or User values, depending on which scope you target.
Instead, retrieve the existing value from the target scope selectively, modify that value (typically by appending a directory, and then write the modified value back to the same scope.
To robustly make the same modification effective in the current process too is nontrivial, given that the in-process copy of $env:Path may have been modified; however, in the simple case of appending a new directory to the user's path, you can simply do $env:Path += ';' + $newDir; you may get away with this simple approach in other cases too, but note that the behavior may be different, given that the order in which the directories are listed in $env:Path matters.
Important: The Path environment variable on Windows is REG_EXPAND_SZ-based by default, so the caveats re the quiet conversion to a static REG_SZ-based value that the code below below performs apply - again, see this answer for a proper, but much more complex solution.
Example:
# New dir. to add to the *user's* path
$newDir = 'c:\foo\bin'
# Get current value *from the registry*
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
# Append the new dir and save back to the registry.
[Environment]::SetEnvironmentVariable('Path', ($userPath + ';' + $newDir), 'User')
# To also update the current process, append $newDir to the in-process
# variable, $env:Path
$env:Path += ';' + $newDir
As an aside: On Unix-like platforms, the separator is :, not ; (reflected in [System.IO.Path]::PathSeparator , and the case-sensitive variable name is Path. As stated, .NET fundamentally doesn't offer persistent environment-variable definitions on Unix-like platforms (as of .NET Core 3.1), because the various platforms have no unified native mechanism for doing so.
[1] On Unix-like platforms, targeting User or Machine is quietly ignored as of .NET Core 3.1
[2] Caveat: New processes created directly by the current PowerShell session (direct invocation, Start-Process, Start-Job) do not yet see the registry changes, because the inherit the current session's environment.
If you take apart that command, you are assigning a new string to the $env:Path variable. The string is:
"$($env.Path);."
When you place a $ followed by a set of parens () within a double-quoted string, it causes PowerShell to evaluate the contents of the parens and put the output of that evaluation into the string. See the help for about_Quoting_Rules in PowerShell help, or here in the section about evaluating expressions. Therefore:
PS C:> $A = "abc"
PS C:> $B = "AB$($A)CD"
PS C:> $B
ABabcCD
So the command you posted is appending ";." to the end of your path. When doing this, the "." will automatically be expanded out to the current directory, so effectively you will be adding a semi-colon plus the current directory to your $env:PATH variable each time you run it.
Hope this helps.
It added to $env:PATH at the process scope, meaning it's not set as a user variable or machine variable and the new value does not exist outside of your current PowerShell session. You will not see an environment variable set in this way from the Environment Variables dialog under System Properties.
If you do want to set a persistent environment variable at the User or Machine scope from PowerShell, you can, but you have to call a special method for this from the Environment class, SetEnvironmentVariable:
# Set system (machine) level environment variable
[Environment]::SetEnvironmentVariable( 'VARIABLE_NAME', 'VARIABLE_VALUE', [EnvironmentVariableTarget]::Machine )
# Set user level environment variable
[Environment]::SetEnvironmentVariable( 'VARIABLE_NAME', 'VARIABLE_VALUE', [EnvironmentVariableTarget]::User )
You can also use this method to set a Process level environment variable, but you can already do this with the $env:VARIABLE_NAME = 'VARIABLE_VALUE' syntax which is idiomatic to PowerShell.
I'm confused because I had already added the folder to system variables path
What probably happened here is that you opened a PowerShell session, then went to the Environment Variables dialog, and set the variable value. Problem is, normally environment variables, including PATH, are only read when the process starts. Most of the time, you just need to restart your PowerShell session to get the new values.
If you have Chocolatey installed, you can use the refreshenv command, which reads the current stored environment variables from the registry, and re-sets the variables in the current process. If you want to implement this sort of thing yourself, here is the source for it. Though it's written as a cmd script, you can reimplement the logic in PowerShell yourself, or just copy the script source yourself to use.
Also, don't add . to your path. While convenient, it circumvents built in PowerShell security measures. Add the directory with your programs/scripts you want to run by command to your PATH directly, or invoke them from the current directory by prepending the command with .\. For example:
.\My-ScriptInThisDirectory.ps1

Unable to set PATH using SetEnvironmentVariable

I am trying to set the environment variable in my powershell script.
Heres my code snippet.It sets the system environment variable (System Properties > Environment variable > Path) however, I am unable to start rabbitmq-service in powershell.
'$ENV:PATH' command output doesn't have the newly added path. After system restart $ENV:PATH contains the new path but the command 'rabbitmq-service' still doesnt work.
# SET Erlang and RabbitMQ Home Path
$ERLANG_HOME = "$env:PROGRAMFILES\erl9.2"
[System.Environment]::SetEnvironmentVariable("ERLANG_HOME", $ERLANG_HOME, "Machine")
$ERTS_HOME = "$env:PROGRAMFILES\erts-9.2"
[System.Environment]::SetEnvironmentVariable("ERTS_HOME", $ERTS_HOME, "Machine")
$RABBITMQ_HOME = "$env:PROGRAMFILES\RabbitMQ Server\rabbitmq_server-3.6.11"
[System.Environment]::SetEnvironmentVariable("RABBITMQ_HOME", $RABBITMQ_HOME, "Machine")
# Add Erlang and RabbitMQ to Path
$System_Path_Elems = [System.Environment]::GetEnvironmentVariable("PATH", "Machine").Split(";")
if (!$System_Path_Elems.Contains("$RABBITMQ_HOME\sbin") -and !$System_Path_Elems.Contains("$ERLANG_HOME\bin") -and !$System_Path_Elems.Contains("$ERTS_HOME\bin"))
{
$newPath = [System.String]::Join(";", $System_Path_Elems + "$ERLANG_HOME\bin" + "$ERTS_HOME\bin" + "$RABBITMQ_HOME\sbin")
[System.Environment]::SetEnvironmentVariable("PATH", $newPath, "Machine")
}
If I set the PATH using $env:PATH as below in my script, It works.
$env:Path += ";C:\\Program Files\\erl9.2\\erts-9.2\\bin;
C:\\Program Files\\RabbitMQ Server\\rabbitmq_server-3.6.11\\sbin;C:\\Program Files\\erl9.2\\bin"
I am able to execute the following commands without any issues.
rabbitmq-service remove
rabbitmq-plugins enable rabbitmq_management --offline
rabbitmq-service install
rabbitmq-service start
So, why 'SetEnvironmentVariable' doesn't work. Am I missing something here?
$env:PATH = ...
is equivalent to (namespace prefix System. implied):
[Environment]::SetEnvironmentVariable(
'PATH',
...,
[EnvironmentVariableTarget]::Process
)
PowerShell automatically converts strings to enumeration values, so 'Process' in lieu of [EnvironmentVariableTarget]::Process works too.
That is, in both cases you're updating the environment variable for the current process only - future sessions will not see the updated value.
By contrast, if you use [Environment]::SetEnvironmentVariable() with the [EnvironmentVariableTarget]::Machine / [EnvironmentVariableTarget]::User targets, you update the persistent definitions machine-wide / for the current user only, without also updating the value in the current process; that is, these definitions only take effect in future sessions.
Unfortunately, there is no single call that would allow you to do both, so you'll need two calls:
# Update the current process' env.var
$env.PATH = ...
# Also persist the new value.
# Note that targeting [EnvironmentVariableTarget]::Machine requires
# ELEVATION (running as admin).
[Environment]::SetEnvironmentVariable('PATH', $env:PATH, <target>)
Caveat:
On Windows, the process-level $env:PATH value is a composite value, the concatenation of the registry-based machine-level and the user-level definitions.
Additionally, just as $env:PATH only contains expanded, literal values - even though he underlying registry entries may be defined by incorporating references to other environment variables (e.g. %SystemRoot%) - [Environment]::SetEnvironmentVariable() only supports writing literal paths.
A proper solution therefore requires reading and writing raw (unresolved) definitions from the registry, as shown in this answer.

Cake Build: Collecting environment variables from cmd file

I want to run a a project with msbuild. The msbuild file contains references to environment variables.
There is a cmd-file which will set these environment variables. I need to call it from my Cake script before.
If I use StartProcess to call this cmd-file before I start the compiler it won't work because Cake does not collect environment variables.
How do I call the cmd-file the correct way?
Contents of the batch-file:
#SET BDS=C:\Program Files (x86)\Embarcadero\RAD Studio\12.0
#SET BDSINCLUDE=C:\Program Files (x86)\Embarcadero\RAD Studio\12.0\include
#SET BDSCOMMONDIR=C:\Users\Public\Documents\RAD Studio\12.0
#SET FrameworkDir=C:\Windows\Microsoft.NET\Framework\v3.5
#SET FrameworkVersion=v3.5
#SET FrameworkSDKDir=
#SET PATH=%FrameworkDir%;%FrameworkSDKDir%;C:\Program Files (x86)\Embarcadero\RAD Studio\12.0\bin;C:\Program Files (x86)\Embarcadero\RAD Studio\12.0\bin64;%PATH%
#SET LANGDIR=DE
The StartProcess Alias has an overload which accepts a ProcessSettings object, which contains an EnvironmentVariables property, which is just a generic dictionary of string and string. This allows you to construct any environment variables that need to be passed to the process that you want to start. An example would be the following:
StartProcess("cmd", new ProcessSettings{
Arguments = "/c set",
EnvironmentVariables = new Dictionary<string, string>{
{ "CI", "True" },
{ "TEMP", MakeAbsolute(Directory("./Temp")).FullPath }
}
});
The same technique could also be used to call MSBuild directly, rather than using the batch file, as the MSBuildSettings object has the same property.

How can I access pre-defined Release Management variables in a Powershell script?

According to the documentation, there are a bunch of predefined variables available for the tasks which run in an environment for Release Management. How do I access these variables from within a Powershell script?
For example: the System.TeamProject variable is defined, and should return the current TFS TeamProject. However, when I write:
Write-Host "environment var: $env:System.TeamProjectId"
The output in the log file is:
2016-06-07T09:26:49.5537161Z environment var: release.TeamProject
However, in the Initialize log file, the following is displayed:
4 2016-06-07T09:26:40.4121001Z Environment variables available are below. Note that these environment variables can be referred to in the task (in the ReleaseDefinition) by replacing "_" with "." e.g. AGENT_NAME environment variable can be referenced using Agent.Name in the ReleaseDefinition:
...
34 2016-06-07T09:26:40.4277002Z [SYSTEM_COLLECTIONID] --> [2043d9ba-7ec9-43f0-8e6c-96a8b28f55d8]
35 2016-06-07T09:26:40.4277002Z [SYSTEM_TEAMPROJECTID] --> [9718773d-2aee-4625-91c6-80de16301479]
36 2016-06-07T09:26:40.4277002Z [SYSTEM_TEAMPROJECT] --> [MyProject]
37 2016-06-07T09:26:40.4277002Z [SYSTEM_CULTURE] --> [en-US]
So this means the variable is there. I tried $(System.TeamProject) as suggested somewhere else, but that fails with The term ... is not recognized-error.
Also, the variables which I have configured myself in the Release Definition, e.g. priority, I am able to access with $env:priority.
As a workaround I can create my own parameters in the script, and pass them in the Arguments field in the task definition, but this kind of defeats the purpose.
Use curly braces because the variable name contains .. Example:
PS C:\> ${Env:System.TeamProject} = "Var contents"
PS C:\> ${Env:System.TeamProject}
Var contents
PS C:\> Write-Host "Prefix - $Env:System.TeamProject"
Prefix - .TeamProject
PS C:\> Write-Host "Prefix - ${Env:System.TeamProject}"
Prefix - Var contents