-
-
Notifications
You must be signed in to change notification settings - Fork 19
/
Copy pathRepair-Path.ps1
292 lines (250 loc) · 8.46 KB
/
Repair-Path.ps1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
<#
.SYNOPSIS
Clean up the PATH environment variable, removing duplicates, empty values, invalid paths, repairs
variable substitutions, and moves paths between User and System paths appropriately.
.PARAMETER Yes
Respond to all prompts automatically with "Yes".
.PARAMETER WhatIf
Run the command and report changes but don't make any changes.
#>
# CmdletBinding adds -Verbose functionality, SupportsShouldProcess adds -WhatIf
[CmdletBinding(SupportsShouldProcess=$true)]
param (
[switch] $Yes
)
Begin
{
$script:MaxLength = [Int16]::MaxValue
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Remove empty and unknown paths from the given collection
# Also remove expanded duplicates of non-expanded variables
function RemoveInvalidPaths
{
param (
[string[]] $paths,
[string[]] $expos,
[string] $target
)
Write-Host "... cleaning $target" -ForegroundColor DarkYellow
$list = @()
foreach ($path in $paths)
{
if ($path -eq '')
{
Write-Host "... removing empty $target path"
}
elseif ($list -contains $path) # -contains is case-insensitive
{
Write-Host "... removing duplicate $target path: $path"
}
elseif ($expos -contains $path)
{
Write-Host "... removing expanded $target path: $path"
}
elseif (Test-Path (ExpandPath $path))
{
$list += $path
}
else
{
Write-Host "... removing invalid $target path: $path"
}
}
$list
}
function ExpandPath ($path)
{
# check env variables in path like '%USREPROFILE%'
$match = [Regex]::Match($path, '\%(.+)\%')
if ($match.Success)
{
$evar = [Environment]::GetEnvironmentVariable( `
$match.Value.Substring(1, $match.Value.Length -2))
if ($evar -and ($evar.Length -gt 0))
{
return $path -replace $match.value, $evar
}
}
return $path
}
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Balances paths between User and System targets, moving user-specific paths to System and
# system-specific paths to User. Paths that exist in both will remain in the User target.
# Consolidate duplicates into high collection from low collection
# Move high prefix paths from low collection into high collection
function BalancePaths
{
param (
[string[]] $highpaths, # preferred collection
[string[]] $highExpos, # entries that contain variables
[string] $highprefix, # prefix that should be moved to preferred collection
[string] $highname, # name of the preferred collection (User or System)
[string[]] $lowpaths, # other collection
[string] $lowprefix, # prefix that should remain in other collection
[string] $lowname # name of the other collection (System or User)
)
Write-Host "... balancing $lowname to $highname" -ForegroundColor DarkYellow
$lpaths = @()
foreach ($path in $lowpaths)
{
$expo = ExpandPath $path
# if path starts with specified highprefix then move it to highpaths
if ($expo.StartsWith($highprefix, 'CurrentCultureIgnoreCase'))
{
if (($highpaths -contains $expo) -or `
($highExpos -contains $path) -or `
($highpaths -contains $path))
{
Write-Host ... ignoring duplicate $lowname path`: "$path"
}
else
{
$highpaths += $path
Write-Host ... moving from $lowname to $highname`: "$path"
}
}
# prefer High over Low if in both
elseif (!$path.StartsWith($lowprefix, 'CurrentCultureIgnoreCase') -and ($highpaths -contains $path))
{
Write-Host ... ignoring duplicate $lowname path`: "$path"
}
else
{
$lpaths += $path
}
}
return $highpaths, $lpaths
}
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Replace hard-coded path prefixes with given environment %variable% name
function InjectVariables ($paths, $name)
{
$value = [System.Environment]::GetEnvironmentVariable($name)
$subst = "%$name%"
$result = @()
$paths | % `
{
if ($_.StartsWith($value, 'CurrentCultureIgnoreCase'))
{
$path = $_.Replace($value, $subst)
Write-Host "... injecting $name`: $path"
$result += $path
}
else
{
$result += $_
}
}
$result
}
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Rebuild the $env:PATH after the System and User paths have been cleaned
# so the current session has the updated PATH settings
function RebuildSessionPath ($sysPaths, $usrPaths)
{
$spaths = $sysPaths | % { ExpandPath $_ }
$upaths = $usrPaths | % { ExpandPath $_ }
$ppaths = @()
$shell = 'PowerShell'
if ($PSVersionTable.PSVersion.Major -lt 6) { $shell = 'WindowsPowerShell' }
# PowerShell scripts are in path
$psroot = Join-Path $env:USERPROFILE "Documents\$shell\Modules\Scripts"
# preserve per-session (process) entries
$env:Path -split ';' | % `
{
if (!(($spaths -contains $_) -or ($upaths -contains $_) -or `
$_.StartsWith($psroot, 'CurrentCultureIgnoreCase')))
{
$ppaths += ExpandPath $_
}
}
$paths = $ppaths + $spaths + $upaths
# ensure PowerShell scripts are in path
if (!($paths -contains $psroot)) { $paths += $psroot }
if ($WhatIfPreference)
{
Write-Host ([String]::New('-',80)) -ForegroundColor DarkYellow
Write-Host 'Original $env:Path' -ForegroundColor DarkYellow
Write-Host (($env:Path -split ';') -join [Environment]::NewLine) -ForegroundColor DarkGray
Write-Host 'Updated env:PATH' -ForegroundColor DarkYellow
Write-Host ($paths -join [Environment]::NewLine) -ForegroundColor DarkGray
}
else
{
$env:Path = $paths -join ';'
}
}
}
Process
{
# In order to avoid substitution of environment variables in path strings
# we must pull the Path property raw values directly from the Registry.
# Other mechanisms such as [Env]::GetEnvVar... will expand variables.
# open keys with write access ($true argument)
$0 = 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment'
$sysKey = [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey($0, $true)
$sysPath = $sysKey.GetValue('Path', $null, 'DoNotExpandEnvironmentNames')
$sysPaths = $sysPath -split ';'
$sysExpos = $sysPaths | ? { $_ -match '\%.+\%' } | % { (ExpandPath $_).ToLower() }
$usrKey = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Environment', $true)
$usrPath = $usrKey.GetValue('Path', $null, 'DoNotExpandEnvironmentNames')
$usrPaths = $usrPath -split ';'
$usrExpos = $usrPaths | ? { $_ -match '\%.+\%' } | % { (ExpandPath $_).ToLower() }
if ($VerbosePreference -eq 'Continue')
{
Write-Host 'Original System Paths' -ForegroundColor DarkYellow
Write-Host ($sysPaths -join [Environment]::NewLine) -ForegroundColor DarkGray
Write-Host 'Original User Paths' -ForegroundColor DarkYellow
Write-Host ($usrPaths -join [Environment]::NewLine) -ForegroundColor DarkGray
Write-Host
}
# cleanup empty and invalid path entries
$sysPaths = RemoveInvalidPaths $sysPaths $sysExpos 'System'
$usrPaths = RemoveInvalidPaths $usrPaths $usrExpos 'User'
# move user paths from System to User
$usrPaths, $sysPaths = BalancePaths $usrPaths $usrExpos $env:USERPROFILE 'User' $sysPaths $env:SystemRoot 'System'
# move system paths from User to System
$sysPaths, $usrPaths = BalancePaths $sysPaths $sysExpos $env:SystemRoot 'System' $usrPaths $env:USERPROFILE 'User'
$sysPaths = InjectVariables $sysPaths 'SystemRoot'
$usrPaths = InjectVariables $usrPaths 'USERPROFILE'
$newSysPath = $sysPaths -join ';'
$newUsrPath = $usrPaths -join ';'
if ($VerbosePreference -eq 'Continue')
{
if ($newSysPath -ne $sysPath)
{
Write-Host 'New System Paths' -ForegroundColor DarkYellow
Write-Host ($sysPaths -join [Environment]::NewLine) -ForegroundColor DarkGray
}
if ($newUsrPath -ne $usrPath)
{
Write-Host 'New User Paths' -ForegroundColor DarkYellow
Write-Host ($usrPaths -join [Environment]::NewLine) -ForegroundColor DarkGray
}
}
if (($newSysPath -ne $sysPath) -or ($newUsrPath -ne $usrPath))
{
if ($WhatIfPreference)
{
# if WhatIfPreference then this will just report
RebuildSessionPath $sysPaths $usrPaths
}
else
{
if ($Yes) { $ans = 'y' } else { $ans = Read-Host 'Apply changes? (Y/N) [Y]' }
if (($ans -eq 'y') -or ($ans -eq 'Y') -or ($ans -eq ''))
{
if ($newSysPath -ne $sysPath) { $sysKey.SetValue('Path', $newSysPath, 'ExpandString') }
if ($newUsrPath -ne $usrPath) { $usrKey.SetValue('Path', $newUsrPath, 'ExpandString') }
RebuildSessionPath $sysPaths $usrPaths
}
}
}
else
{
Write-Host
Write-Host 'NO changes needed'
}
$sysKey.Dispose()
$usrKey.Dispose()
}