How to set Rust's cargo directory in windows PATH? - windows

I want unnatended install of rust, so I did this little script:
Write-Host "Installing Rust..." -ForegroundColor Cyan
$exePath = "$env:TEMP\rustup-init.exe"
Write-Host "Downloading..."
(New-Object Net.WebClient).DownloadFile('https://static.rust-lang.org/rustup/dist/x86_64-pc-windows-msvc/rustup-init.exe', $exePath)
Write-Host "Installing..."
cmd /c start /wait $exePath -y
Remove-Item $exePath
$addPath = "$env:USERPROFILE\.cargo\bin"
[Environment]::SetEnvironmentVariable('PATH', $env:PATH, $addPath)
but I get
virtualbox-iso: Cannot convert argument "target", with value: "C:\Users\vagrant\.cargo\bin", for "SetEnvironmentVariable" to type
virtualbox-iso: "System.EnvironmentVariableTarget": "Cannot convert value "C:\Users\vagrant\.cargo\bin" to type
virtualbox-iso: "System.EnvironmentVariableTarget". Error: "Unable to match the identifier name C:\Users\vagrant\.cargo\bin to a valid
it looks like a text cannot be converted to a PATH type? What does it mean?

The immediate solution to your problem is that you need to call [Environment]::SetEnvironmentVariable() as follows:
# Modify the user-level PATH definition.
# To modify the machine-level definition, use 'Machine' instead of user 'User',
# but you then need to run with ELEVATION (as admin).
# !! SEE CAVEATS BELOW.
[Environment]::SetEnvironmentVariable('PATH', ($env:PATH + ";$addPath"), 'User')
That is, as Herohtar points out, the method expects only a complete, new value, as the second parameter, and its third parameter specifies the target scope for the persistent, registry-based environment-variable definition.
Caveats:
The process-level $env:PATH value is a composite value of the machine-level and user-level definitions in the registry, so with the command above you're in effect duplicating entries from the respective other scope.
Additionally, redefining the variable this way replaces any entries in the value that are defined in terms of other environment variables with the expanded, literal values.
$env:PATH contains expanded values to begin with, but even reading the unexpanded value directly from the registry won't work, because [Environment]::SetEnvironmentVariable() invariably writes the given string as a REG_SZ value (literal string) rather than as an REG_EXPAND_SZ value (string that may contain %FOO%-style references to other environment variables). Similarly, [Environment]::GetEnvironmentVariable() only reports expanded values.
A proper solution requires quite a bit of extra work, as discussed in this answer, which contains helper function Add-Path.
A pragmatic shortcut - if you're willing to accept that your registry definitions are converted to literal strings - is the following:
Read the current definition - for the target scope only - from the registry, via [Environment]::GetEnvironmentVariable()
Add to this scope-specific value and save it back to the same scope.
Additionally, you may want to add the new entry to the in-process definition of $env:PATH so that the change takes immediate effect.
Here is the solution in the context of a streamlined version of your code:
Write-Host "Installing Rust..." -ForegroundColor Cyan
$exePath = "$env:TEMP\rustup-init.exe"
Write-Host "Downloading..."
Invoke-WebRequest 'https://static.rust-lang.org/rustup/dist/x86_64-pc-windows-msvc/rustup-init.exe' -OutFile $exePath
Write-Host "Installing..."
& $exePath -y
Remove-Item $exePath
$addPath = "$env:USERPROFILE\.cargo\bin"
$scope = 'User' # Change to 'Machine', if needed, which then requires ELEVATION.
[Environment]::SetEnvironmentVariable(
'PATH',
([Environment]::GetEnvironmentVariable('PATH', $scope) + ";$addPath"),
$scope
)
# Also update the current process' definition
$env:PATH += ";$addPath"

Related

Reference default path in powershell script

Is there a way to call the system path if the path has been changed in the current terminal? i.e.:
$env:Path = "C:\some new path"
#some coding that requires a different path set up
$env:Path = $defaultPath #would have to define $defaultPath by calling the system default path
Use the following to reload the $env:PATH environment variable from the registry, as future sessions would see it (assuming no further relevant registry updates are made).
If your current session hasn't made any relevant registry updates, this is the same as getting the value that was in effect on session startup - barring any dynamic additions via a $PROFILE script:
$env:PATH = [Environment]::GetEnvironmentVariable('Path', 'Machine'),
[Environment]::GetEnvironmentVariable('Path', 'User') -join ';'
Note:
A process' effective $env:PATH value is a composite value of a machine-level and a user-level registry entry, with the machine-level definition taking precedence, as reflected in the two .NET API calls above.
Note that the underlying registry locations - HKEY_LOCAL_MACHIN\System\CurrentControlSet\Control\Session Manager\Environment and HKEY_CURRENT_USER\Environment - are REG_EXPAND_SZ registry values, i.e. they may be defined in terms of other environment variables, such as %SystemRoot% and %ProgramFiles%.
Both the .NET API calls above - using [Environment]::GetEnvironmentVariable() - and PowerShell's Get-ItemProperty and Get-ItemPropertyValue cmdlets expand (interpolate) such references and return verbatim paths - which is what new processes see by default too.
Given the above, the only way to robustly retrieve the value that was in effect at session startup time is to save it in a variable at startup time.
It's still stored in the registry so you can just query it:
Located: HKLM:\System\CurrentControlSet\Control\Session Manager\Environment
$key = "HKCU:\Environment",
"HKLM:\System\CurrentControlSet\Control\Session Manager\Environment"
(Get-ItemPropertyValue -Path $key -Name Path) -Join ';'
Querying the key using Get-ItemPropertyValue (as suggested by Mklement) will give you just the property's value.

Adding directory to Systemvariables Path in CMD

Iam trying to add a directory permanently to Path via CMD. When i try to use the command:
setx path "%path%;C:\Program Files (x86)\chromedriver\chromedriver.exe"
it only saves it to the uservariables Path. Is there a way to add it to the systemvariables Path by using CMD?
This is PowerShell code. There is probably some way to do it with a cmd-only reg.exe, but I have not been down that path. If you are on a supported Windows system, PowerShell will be available.
To do this, you will want to retrieve the current variable values before interpolation (resolving). To do that for the user PATH variable:
(Get-Item -Path 'HKCU:\Environment').GetValue(
'PATH', # the registry-value name
$null, # the default value to return if no such value exists.
'DoNotExpandEnvironmentNames' # the option that suppresses expansion
)
To get the system PATH variable:
(Get-Item -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment').GetValue(
'PATH', # the registry-value name
$null, # the default value to return if no such value exists.
'DoNotExpandEnvironmentNames' # the option that suppresses expansion
)
Once you have the current, pre-interpolation PATH variable value, it can be changed before using Set-Item or setx.exe. Setting the system path will probably require Administrator permission, or should.

Adding things to the user PATH | Powershell SETX Error [duplicate]

I followed this procedure in order to permanently add a path to SumatraPDF using powershell. The last few commands from the link are meant to check that the path has indeed been added.
When I access the path using the following command,
(get-itemproperty -path 'Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment' -Name PATH).Path.split(';')
the result includes the path to SumatraPDF
C:\Windows\system32
C:\Windows
C:\Windows\System32\Wbem
C:\Windows\System32\WindowsPowerShell\v1.0\
C:\Windows\System32\OpenSSH\
C:\ProgramData\chocolatey\bin
C:\texlive\2021\bin\win32
C:\Users\921479\AppData\Local\SumatraPDF
However when I access it using the following command,
($env:path).split(';')
the result does not contain the path to SumatraPDF:
C:\Windows\system32
C:\Windows
C:\Windows\System32\Wbem
C:\Windows\System32\WindowsPowerShell\v1.0\
C:\Windows\System32\OpenSSH\
C:\ProgramData\chocolatey\bin
C:\texlive\2021\bin\win32
C:\Users\921479\AppData\Local\Microsoft\WindowsApps
Finally, actually passing sumatrapdf does not works, which indicates to me that the real path is the one accessed using the get-itemproperty command.
Why does the path set in the registry not correspond to the one set in $env:path? Is there a mistake in the procedure shown in the link I followed? How can I correct it?
I should mention I have already tried restarting the shell but it doesn't help.
Note:
See the middle section for helper function Add-Path
See the bottom section for why use of setx.exe should be avoided for updating the Path environment variable.
The procedure in the linked blog post is effective in principle, but is missing a crucial piece of information / additional step:
If you modify environment variables directly via the registry - which, unfortunately, is the right way to do it for REG_EXPAND_SZ-based environment variables such as Path - you need to broadcast a WM_SETTINGCHANGE message so that the Windows (GUI) shell (and its components, File Explorer, the taskbar, the desktop, the Start Menu, all provided via explorer.exe processes) is notified of the environment change and reloads its environment variables from the registry. Applications launched afterwards then inherit the updated environment.
If this message is not sent, future PowerShell sessions (and other applications) won't see the modification until the next logon / reboot.
Unfortunately, there's no direct way to do this from PowerShell, but there are workarounds:
Brute-force workaround - simple, but visually disruptive and closes all open File Explorer windows:
# Kills all explorer.exe processes, which restarts the Windows shell
# components, forcing a reload of the environment from the registry.
Stop-Process -Name explorer
Workaround via .NET APIs:
# Create a random name assumed to be unique
$dummyName = [guid]::NewGuid().ToString()
# Set an environment variable by that name, which makes .NET
# send a WM_SETTINGCHANGE broadcast
[Environment]::SetEnvironmentVariable($dummyName, 'foo', 'User')
# Now that the dummy variable has served its purpose, remove it again.
# (This will trigger another broadcast, but its performance impact is negligible.)
[Environment]::SetEnvironmentVariable($dummyName, [NullString]::value, 'User')
Workaround by calling the Windows API via an ad hoc-compiled P/Invoke call to SendMessageTimeout() in C#, via Add-Type:
While this is a proper solution, it invariably incurs a noticeable performance penalty due to the ad hoc-compilation the first time it is run in a session.
For details, see this blog post.
The approach in the blog post has another problematic aspect:
It retrieves the expanded environment-variable value from the registry, because that is what Get-ItemProperty and Get-ItemPropertyValue invariably do. That is, if directories in the value are defined in terms of other environment variables (e.g., %SystemRoot% or %JAVADIR%), the returned value no longer contains these variables, but their current values. See the bottom section for why this can be problematic.
The helper function discussed in the next section addresses all issues, while also ensuring that the modification takes effect for the current session too.
The following Add-Path helper function:
Adds (appends) a given, single directory path to the persistent user-level Path environment variable by default; use -Scope Machine to target the machine-level definition, which requires elevation (run as admin).
If the directory is already present in the target variable, no action is taken.
The relevant registry value is updated, which preserves its REG_EXPAND_SZ data type, based on the existing unexpanded value - that is, references to other environment variables are preserved as such (e.g., %SystemRoot%), and may also be used in the new entry being added.
Triggers a WM_SETTINGCHANGE message broadcast to inform the Windows shell of the change.
Also updates the current session's $env:Path variable value.
Note: By definition (due to use of the registry), this function is Windows-only.
With the function below defined, your desired Path addition could be performed as follows, modifying the current user's persistent Path definition:
Add-Path C:\Users\921479\AppData\Local\SumatraPDF
If you really want to update the machine-level definition (in the HKEY_LOCAL_MACHINE registry hive, which doesn't make sense with a user-specific path), add -Scope Machine, but not that you must then run with elevation (as admin).
Add-Path source code:
function Add-Path {
param(
[Parameter(Mandatory, Position=0)]
[string] $LiteralPath,
[ValidateSet('User', 'CurrentUser', 'Machine', 'LocalMachine')]
[string] $Scope
)
Set-StrictMode -Version 1; $ErrorActionPreference = 'Stop'
$isMachineLevel = $Scope -in 'Machine', 'LocalMachine'
if ($isMachineLevel -and -not $($ErrorActionPreference = 'Continue'; net session 2>$null)) { throw "You must run AS ADMIN to update the machine-level Path environment variable." }
$regPath = 'registry::' + ('HKEY_CURRENT_USER\Environment', 'HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment')[$isMachineLevel]
# Note the use of the .GetValue() method to ensure that the *unexpanded* value is returned.
$currDirs = (Get-Item -LiteralPath $regPath).GetValue('Path', '', 'DoNotExpandEnvironmentNames') -split ';' -ne ''
if ($LiteralPath -in $currDirs) {
Write-Verbose "Already present in the persistent $(('user', 'machine')[$isMachineLevel])-level Path: $LiteralPath"
return
}
$newValue = ($currDirs + $LiteralPath) -join ';'
# Update the registry.
Set-ItemProperty -Type ExpandString -LiteralPath $regPath Path $newValue
# Broadcast WM_SETTINGCHANGE to get the Windows shell to reload the
# updated environment, via a dummy [Environment]::SetEnvironmentVariable() operation.
$dummyName = [guid]::NewGuid().ToString()
[Environment]::SetEnvironmentVariable($dummyName, 'foo', 'User')
[Environment]::SetEnvironmentVariable($dummyName, [NullString]::value, 'User')
# Finally, also update the current session's `$env:Path` definition.
# Note: For simplicity, we always append to the in-process *composite* value,
# even though for a -Scope Machine update this isn't strictly the same.
$env:Path = ($env:Path -replace ';$') + ';' + $LiteralPath
Write-Verbose "`"$LiteralPath`" successfully appended to the persistent $(('user', 'machine')[$isMachineLevel])-level Path and also the current-process value."
}
The limitations of setx.exe and why it shouldn't be used to update the Path environment variable:
setx.exe has fundamental limitations that make it problematic, particularly for updating environment variables that are based on REG_EXPAND_SZ-typed registry values, such as Path:
Values are limited to 1024 characters, with additional ones getting truncated, albeit with a warning (as of at least Windows 10).
The environment variable that is (re)created is invariably of type REG_SZ, whereas Path is originally of type REG_EXPAND_SZ and contains directory paths based on other environment variables, such as %SystemRoot% and %JAVADIR%.
If the replacement value contains only literal paths (no environment-variable references) that may have no immediate ill effects, but, for an instance, an entry that originally depended on %JAVADIR% will stop working if the value of %JAVADIR% is later changed.
Additionally, if you base the updated value on the current session's $env:Path value, you'll end up duplicating entries, because the process-level $env:Path value is a composite of the machine-level and current-user-level values.
This increases the risk of running into the 1024-character limit, especially if the technique is used repeatedly. It also bears the risk of duplicate values lingering after the original entry is removed from the original scope.
While you can avoid this particular problem by retrieving the scope-specific value either directly from the registry or - invariably in expanded form - via [Environment]::GetEnvironmentVariable('Path', 'User') or [Environment]::GetEnvironmentVariable('Path', 'Machine'), that still doesn't solve the REG_EXPAND_SZ problem discussed above.
Use setx to permanently update an environment variable. Don't hack the registry.
After you invoke setx, just update the Path environment manually in the current session. Powershell: Reload the path in PowerShell

Create a potocol handler for a Powershell script

How can I create a protocol handler for a powershell script and make the target powershell script receive command line arguments?
And what are the security concerns in doing so?
I thought I write up a decent guide on doing so since the information I found online was lacking some details.
https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85)
First off the security concerns
Any program, website, script etc. that is running on your computer can set off the protocol. There are no authorization checks.
You should NOT create a universal protocol handler. That would be a massive security issue concern. I mean that would enable a program, website, script etc. to run any powershell script or command on your computer.
Creating the protocol handler in Windows registry
The protocol must be registered in Windows Registry. It's a simple task.
I'm calling my powershell protocol handler for pwsh
Step 1: Open the Registry Editor and navigate to
Computer\HKEY_CLASSES_ROOT
For inspiration you can look at Computer\HKEY_CLASSES_ROOT\http to look at how that protocol handler is made.
Step 2: Create the following hierarchy:
Create the key pwsh: [Computer\HKEY_CLASSES_ROOT\pwsh]
Edit the default value of (Default) to URL:pwsh. Remember I call my protocol handler for pwsh, write whatever your is called.
Add a string value with the name URL Protocol and empty data.
It should look like this now:
Create a new key under pwsh, DefaultIcon: Computer\HKEY_CLASSES_ROOT\pwsh\DefaultIcon.
Set the (Default) data field to a filepath that leads to an icon or image. I used the powershell icon for Powershell 7 C:\Program Files (x86)\PowerShell\7-preview\assets\ps_black_32x32.ico.
Then create the keys shell -> open -> command like shown on the image above.
In the key command change the (Default) data value to where powershell is installed and then the powershell script to be run.
When testing I do this: "C:\Program Files\PowerShell\6\pwsh.exe" -noexit -executionpolicy bypass -Command {Write-Host %1}
Note I am using powershell core 6 and your path to powershell is probably different.
You can test to check if it works by opening the run program in Windows(Windows+R).
Expected behavior is the powershell window to open with the text pwsh:Hello Stackoverflow printed.
Step 3: Create a powershell script to handle incoming actions on the protocol.
The production ready data value for the command key: "C:\Program Files\PowerShell\6\pwsh.exe" -noexit -File C:\handleActions.ps1 %1
Param($Argument="") # If the protocol is ran you always at least get the protocol name as an argument. (if using the %1)
[String]
$Argument
function Handle-Actions { # The cmdlet 'Handle-Actions' uses an unapproved verb.
[cmdletBinding()]
param(
[Parameter(Mandatory=$false, Position=0)]
[String]
$Argument
)
$Argumuments = $Argument.Split([Char]0x003F) # Splits by `?`
#Argumnets is now in an array, do whatever you need to next.
$Argumuments | %{
Write-Host $_ # Writes each argument that was seperated by ? to a line
}
}
Handle-Actions -Argument $Argument
Given the run command pwsh:?firstArgument?SecondArgument the script will output:
pwsh:
firstArgument
SecondArgument
To complement your helpful guide with sample code that automates creation of a custom protocol handler:
The following:
Creates a custom URI protocol custom: (rather than pwsh:, given that PowerShell is simply used to implement the protocol) to which an open-ended number of arguments can be passed.
Does so for the current user only (HKEY_CURRENT_USER\Software\Classes) by default; however, it's easy to tweak the code to implement the custom protocol for all users instead (HKEY_LOCAL_MACHINE\Software\Classes), though you'll need to run the code with elevation (as administrator) then.
A handler *.ps1 script is automatically created:
At $env:USERPROFILE\customUriHandler.ps1 in the current-user scenario.
At $env:ALLUSERPROFILE\customUriHandler.ps1 in the all-users scenario.
The handler script simply echoes the arguments passed to it, and it is invoked in a PowerShell script window that is kept open after script execution (-NoExit); tweak the PowerShell command as needed.
The protocol expects its arguments as if it were a shell command, i.e., as a space-separated list of arguments, with argument-individual "..." quoting, if necessary.
The sample command at the end uses Start-Process to invoke the following URI, which you could also submit from the Run dialog (WinKey-R), which passes arguments one, two & three, four:
URI: custom:one "two & three" four
Invocation via Start-Process: Start-Process 'custom:one "two & three" four'
Caveat: If you submit this URI via a web browser's address bar (note: doesn't seem to work with Microsoft Edge), it is URI-escaped, and a single one%20%22two%20&%20three%22%20four argument is passed instead, which would require custom parsing; similarly, submitting from File Explorer's address bar passes one%20two%20&%20three%20four, though note that the " chars. are - curiously - lost in the process.
# Determine the scope:
# Set to $false to install machine-wide (for all users)
# Note: Doing so then requires running with ELEVATION.
$currentUserOnly = $true
if (-not $currentUserOnly) {
net session *>$null
if ($LASTEXITCODE) { Throw "You must run this script as administrator (elevated)." }
}
$ErrorActionPreference = 'Stop'
# The name of the new protocol scheme
$schemeName = 'custom'
$pwshPathEscaped = (Get-Process -Id $PID).Path -replace '\\', '\\'
$handlerScript = ($env:ALLUSERSPROFILE, $env:USERPROFILE)[$currentUserOnly] + "\${schemeName}UriHandler.ps1"
$handlerScriptEscaped = $handlerScript -replace '\\', '\\'
# Create the protocol handler script.
#'
# Remove the protocol scheme name from the 1st argument.
$argArray = $args.Clone()
$argArray[0] = $argArray[0] -replace '^[^:]+:'
# If the 1st argument is now empty, remove it.
if ('' -eq $argArray[0]) { $argArray = $argArray[1..($argArray.Count-1)] }
"Received $($argArray.Count) argument(s)."
$i = 0
foreach ($arg in $argArray) {
"#$((++$i)): [$arg]"
}
'# > $handlerScript
# Construct a temp. *.reg file.
# Target the scope-appropriate root registrykey.
$rootKey = ('HKEY_LOCAL_MACHINE\Software\Classes', 'HKEY_CURRENT_USER\Software\Classes')[$currentUserOnly]
# Determine a temp. file path.
$tempFile = [IO.Path]::GetTempPath() + [IO.Path]::GetRandomFileName() + '.reg'
#"
Windows Registry Editor Version 5.00
[$rootKey\$schemeName]
#="URL:$schemeName"
"URL Protocol"=""
[$rootKey\$schemeName\DefaultIcon]
#="$pwshPathEscaped"
[$rootKey\$schemeName\shell]
#="open"
[$rootKey\$schemeName\shell\open\command]
; === Tweak the PowerShell command line here: ===
#="\"$pwshPathEscaped\" -ExecutionPolicy Bypass -NoProfile -NoExit -File \"$handlerScriptEscaped\" %1"
"# > $tempFile
# Import the *.reg file into the registry.
& {
$ErrorActionPreference = 'Continue'
reg.exe import $tempFile 2>$null
if ($LASTEXITCODE) { Throw "Importing with reg.exe failed: $tempFile"}
}
# Remove the temp. *.reg file.
Remove-Item -ErrorAction Ignore -LiteralPath $tempFile
# ---
# Sample invocation of the new protocol with 3 arguments:
$uri = "$schemeName`:one `"two & three`" four"
Write-Verbose -Verbose "Invoking the following URI: $uri"
Start-Process $uri

Admin vs Non-Admin Mode - Cannot overwrite variable because the variable has been optimized

During some testing today I came across an unexpected issue and I do not understand why it is happening. Below is the code I am using to duplicate the issue. It is only a very small portion of the larger project.
Testing is being cone on Windows 10 Build 1709, if that helps
Both the PS1 File and the BAT File are named the same.
Ways to cause the errors
Running the PS1 File via Right-Click - Run with PowerShell will cause the error
Opening PowerShell ISE in Non-Admin Mode, then opening/running the script will cause the error
Running BAT File as Admin or Non-Admin will cause the error
Ways to avoid the errors
Opening PowerShell ISE in Admin Mode, then opening/running the script will not cause the error
Adding Script: in front of the variables on the last 2 lines of code will not cause the error no matter how the script is executed
Using VSCode, it will work as shown below. Running it in the integrated terminal, it will see it not running as an Admin, it will launch PowerShell.exe outside of VSCode and work without issue
-
Why do I have Script: in front of the variables in the functions? It was the only way I could get variables set in the functions to be used outside the functions. The other 25 or so variables not listed in this post do not have an issue, however, they are not modified like these two are after they are set.
The Questions
Why, if running the ISE in Admin Mode, it will work?
Why would it not work if it relaunches as an Administrator?
Why does VSCode not care and it works regardless?
Something isn't making sense and I cannot pinpoint it.
Here are the errors
Cannot overwrite variable NetFX3 because the variable has been optimized. Try using the New-Variable or Set-Variable
cmdlet (without any aliases), or dot-source the command that you are using to set the variable.
At C:\Users\a502690530\Desktop\Testing2.ps1:14 char:5
+ [string]$Script:NetFX3 = $BAT_Files_Path + "NetFX3.zip"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : WriteError: (NetFX3:String) [], SessionStateUnauthorizedAccessException
+ FullyQualifiedErrorId : VariableNotWritableRare
Cannot overwrite variable Power_Plan because the variable has been optimized. Try using the New-Variable or
Set-Variable cmdlet (without any aliases), or dot-source the command that you are using to set the variable.
At C:\Users\a502690530\Desktop\Testing2.ps1:15 char:5
+ [string]$Script:Power_Plan = $BAT_Files_Path + "Power_Plan.zip"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : WriteError: (Power_Plan:String) [], SessionStateUnauthorizedAccessException
+ FullyQualifiedErrorId : VariableNotWritableRare
Here is the code
# Checks if running as an administrator. If not, it will relaunch as an administrator
If (-Not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
$Arguments = "& '" + $MyInvocation.MyCommand.Definition + "'"
Start-Process Powershell -Verb RunAs -ArgumentList $Arguments
Break
}
[string]$ErrorActionPreference = "Continue"
[string]$BAT_Files = $Root_Path + "BAT_Files\"
Function Set-FilePaths ([string]$BAT_Files_Path) {
# BAT Files Paths (ZIPs only!!!)
[string]$Script:NetFX3 = $BAT_Files_Path + "NetFX3.zip"
[string]$Script:Power_Plan = $BAT_Files_Path + "Power_Plan.zip"
Set-Lists
}
function Set-Lists {
# List of BAT Files (ZIPs)
[System.Collections.ArrayList]$Script:List_Of_BAT_Files = #(
$NetFX3
$Power_Plan
)
}
Set-FilePaths `
-BAT_Files_Path $BAT_Files
PAUSE
$NetFX3 = ((Split-Path $NetFX3 -Parent) + "\NetFX3\")
$Power_Plan = ((Split-Path $Power_Plan -Parent) + "\Power_Plan\")
BAT File to launch
REG ADD "HKLM\SOFTWARE\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell" /T REG_SZ /V ExecutionPolicy /D Unrestricted /F
Start PowerShell.exe -Command "& '%~dpn0.ps1'"
I have no specific answer, but a pointer:
Your issue sounds like a PowerShell bug related to the DLR (Dynamic Language Runtime), a technology PowerShell uses behind the scenes (since v3); there's at least one open bug report on GitHub that sounds related.
Aside from the workaround you already know of - using scope modifier script consistently - I suggest avoiding variable access across scope boundaries as a general best practice, which should also avoid the problem.
PowerShell is very flexible in what it can return (output) from a function, so it's better to set variables in the caller's scope based on a function's output.
Specifically, I suggest refactoring your code as follows:
Function Get-FilePaths ([string]$BAT_Files_Path) {
# Output the paths as an *array*.
($BAT_Files_Path + "NetFX3.zip"), ($BAT_Files_Path + "Power_Plan.zip")
}
# Call the function in the script scope and capture its output in variables.
$List_Of_BAT_Files = Get-FilePaths
# Use a destructuring assignment to store the elements of the array
# in individual variables
$NetFX3, $Power_Plan = $List_Of_BAT_Files
If there are a lot of individual variables to set, you can make the function output a hash table instead, and use the hash table's named entries instead of individual variables (requires PSv3+, due to use of [ordered] to create a hash table with ordered keys):
Function Get-FilePaths ([string]$BAT_Files_Path) {
# Output the paths as a *hash table*, using its
# entries for named access instead of individual variables.
$outHash = [ordered] #{
NetFX3 = $BAT_Files_Path + "NetFX3.zip"
Power_Plan = $BAT_Files_Path + "Power_Plan.zip"
}
# Add a 'List' entry that contains all values added above as an array.
# Note the need to use #(...) to force creation of a new array from the
# hash table's value collection.
$outHash.List = #($outHash.Values)
# Output the hash table.
$outHash
}
# Call the function in the script scope and capture its output in
# a single variable that receives the hash table.
$hash = Get-FilePaths
# Now you can access the invididual values by name - e.g., $hash.NetFX3 -
# or use $hash.List to get all values.

Resources