Dismounting.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
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338

function Remove-EnvironmentModule
{
    <#
    .SYNOPSIS
    Remove the environment module that was previously imported
    .DESCRIPTION
    This function will remove the environment module from the scope of the console.
    .PARAMETER ModuleFullName
    The name of the environment module to remove.
    .PARAMETER Force
    If this value is set, the module is unloaded even if other modules depend on it. If the delete flag is specified, no conformation
    is required if the Force flag is set.
    .PARAMETER Delete
    If this value is set, the module is deleted from the file system.
    .PARAMETER SkipCacheUpdate
    Only relevant if the delete flag is specified. If SkipCacheUpdate is passed, the Update-EnvironmentModule function is not called.
    .OUTPUTS
    No outputs are returned.
    #>

    [CmdletBinding()]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    Param(
        [switch] $Force,
        [switch] $Delete,
        [switch] $SkipCacheUpdate
    )
    DynamicParam {
        $runtimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
        $validationSet = @()

        if($Delete) {
            $validationSet = $script:environmentModules.Keys
        }
        else {
            $validationSet = Get-LoadedEnvironmentModules | Select-Object -ExpandProperty FullName
        }

        Add-DynamicParameter 'ModuleFullName' String $runtimeParameterDictionary -Mandatory $True -Position 0 -ValidateSet $validationSet
        return $runtimeParameterDictionary
    }

    begin {
        # Bind the parameter to a friendly variable
        $ModuleFullName = $PsBoundParameters['ModuleFullName']
    }

    process {
        if(Test-EnvironmentModuleLoaded $ModuleFullName) {
            Remove-RequiredModulesRecursive -ModuleFullName $ModuleFullName -UnloadedDirectly $True -Force $Force
        }

        if($Delete) {
            if(-not $Force) {
                $result = Show-ConfirmDialogue "Would you really like to delete the environment module '$ModuleFullName' from the file system?"
                if(-not $result) {
                    return
                }
            }

            $module = Get-EnvironmentModule -ListAvailable $ModuleFullName

            if(-not $module) {
                return
            }

            Remove-Item -Recurse -Force $module.ModuleBase

            if(-not $SkipCacheUpdate) {
                Update-EnvironmentModuleCache
            }
        }
    }
}

function Remove-RequiredModulesRecursive
{
    <#
    .SYNOPSIS
    Remove the environment module with the given name from the environment.
    .DESCRIPTION
    This function will remove the environment module from the scope of the console and will later iterate over all required modules to remove them as well.
    .PARAMETER ModuleFullName
    The full name of the environment module to remove.
    .PARAMETER UnloadedDirectly
    This value indicates if the module was unloaded by the user (directly) or if it was a dependency with reference counter decreased to 0 (indirectly).
    .PARAMETER Force
    If this value is set, the module is unloaded even if other modules depend on it.
    .OUTPUTS
    No outputs are returned.
    #>

    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    param(
        [String] $ModuleFullName,
        [Bool] $UnloadedDirectly,
        [switch] $Force
    )
    $name = (Split-EnvironmentModuleName $ModuleFullName).Name

    if(!$script:loadedEnvironmentModules.ContainsKey($name)) {
        Write-InformationColored -InformationAction 'Continue' "Module $name not found"
        return
    }

    $module = $script:loadedEnvironmentModules.Get_Item($name)

    if(!$Force -and $UnloadedDirectly -and !$module.IsLoadedDirectly) {
        Write-Error "Unable to remove module $Name because it was imported as dependency"
        return
    }

    $module.ReferenceCounter--

    Write-Verbose "The module $($module.Name) has now a reference counter of $($module.ReferenceCounter)"

    foreach ($refModule in $module.Dependencies) {
        Remove-RequiredModulesRecursive $refModule.ModuleFullName $False
    }

    if($module.ReferenceCounter -le 0) {
        Write-Verbose "Removing Module $($module.Name)"
        Dismount-EnvironmentModule -Module $module
    }
}

function Dismount-EnvironmentModule([EnvironmentModuleCore.EnvironmentModule] $Module, [switch] $SuppressOutput)
{
    <#
    .SYNOPSIS
    Remove all the aliases and environment variables that are stored in the given module object from the environment.
    .DESCRIPTION
    This function will remove all aliases and environment variables that are defined in the given EnvironmentModule-object from the environment. An error
    is written if the module was not loaded. Either specify the concrete environment module object or the name of the environment module you want to remove.
    .PARAMETER Module
    The module that should be removed.
    #>

    process {
        if(!$loadedEnvironmentModules.ContainsKey($Module.Name))
        {
            Write-InformationColored -InformationAction 'Continue' ("The Environment-Module $inModule is not loaded.") -ForegroundColor $Host.PrivateData.ErrorForegroundColor -BackgroundColor $Host.PrivateData.ErrorBackgroundColor
            return
        }

        Write-Verbose "Identified $($Module.Paths.Length) paths"
        foreach ($pathInfo in $Module.Paths)
        {
            [String] $joinedValue = $pathInfo.Values -join [IO.Path]::PathSeparator
            Write-Verbose "Handling path for variable $($pathInfo.Variable) with values '$joinedValue'"
            if($joinedValue -eq "")  {
                continue
            }

            if ($pathInfo.PathType -eq [EnvironmentModuleCore.PathType]::PREPEND) {
                $pathInfo.Values | ForEach-Object {Remove-EnvironmentVariableValue -Variable $pathInfo.Variable -ModuleValue $_}
            }
            if ($pathInfo.PathType -eq [EnvironmentModuleCore.PathType]::APPEND) {
                $pathInfo.Values | ForEach-Object {Remove-EnvironmentVariableValue -Variable $pathInfo.Variable -ModuleValue $_ -Reverse}
            }
            if ($pathInfo.PathType -eq [EnvironmentModuleCore.PathType]::SET) {
                $previousValue = $script:loadedEnvironmentModuleSetPaths[$Module.FullName]
                $newValue = $null
                if($null -ne $previousValue) {
                    $previousValue = $previousValue[$pathInfo.Variable]

                    if($null -ne $previousValue) {
                        $actualValue = [Environment]::GetEnvironmentVariable($pathInfo.Variable)
                        if($actualValue -ne $pathInfo.Values[0]) {
                            $newValue = $actualValue
                        }
                        else {
                            $newValue = $previousValue
                        }
                    }
                    else {
                        Write-Warning "Unable to find previous set path value for variable '$($pathInfo.Variable)' and module '$($Module.FullName)'"
                    }
                }
                else {
                    Write-Warning "Unable to find previous set path values for module '$($Module.FullName)'"
                }

                [Environment]::SetEnvironmentVariable($pathInfo.Variable, $newValue, "Process")
            }
        }

        $script:loadedEnvironmentModuleSetPaths.Remove($Module.FullName)

        foreach ($alias in $Module.Aliases.Keys) {
            Remove-Item alias:$alias
        }

        foreach ($functionInfo in $Module.Functions.Values) {
            Remove-EnvironmentModuleFunction $functionInfo
        }

        foreach ($parameterName in $Module.Parameters.Keys) {
            Remove-EnvironmentModuleParameterInternal($parameterName)
        }

        $loadedEnvironmentModules.Remove($Module.Name)
        Write-Verbose ("Removing " + $Module.Name + " from list of loaded environment variables")

        Write-Verbose "Removing module $($Module.FullName)"
        Remove-Module $Module.FullName -Force

        if($script:configuration["ShowLoadingMessages"] -and (-not $script:silentUnload)) {
            Write-InformationColored -InformationAction 'Continue' ($Module.Name + " unloaded") -Foregroundcolor $Host.PrivateData.VerboseForegroundColor -BackgroundColor $Host.PrivateData.VerboseBackgroundColor
        }

        return
    }
}

function Remove-EnvironmentVariableValue
{
    <#
    .SYNOPSIS
    Remove the given value from the desired environment variable.
    .DESCRIPTION
    This function will remove the given value from the environment variable with the given name. If the value is not part
    of the environment variable, no changes are performed.
    .PARAMETER Variable
    The name of the environment variable that should be extended.
    .PARAMETER ModuleValue
    The value that was added by the module and that should be removed from the environment variable.
    .PARAMETER Reverse
    The last occurence will be removed if this value is set.
    .OUTPUTS
    No output is returned.
    #>

    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    param(
        [String] $Variable,
        [String] $ModuleValue,
        [Switch] $Reverse
    )

    $actualValue = [environment]::GetEnvironmentVariable($Variable,"Process")
    if(($null -eq $actualValue) -or ($null -eq $ModuleValue)) {
        return
    }
    Write-Verbose "Removing value '$ModuleValue' from environment variable '$Variable'. Reverse search is set to '$Reverse'"

    $actualValuePartsMapping = [System.Collections.Generic.Dictionary[string, int]]::new()  # Mapping of each PATH value to its index
    [System.Collections.Generic.List[string]] $allPathValues = $actualValue.Split([IO.Path]::PathSeparator)

    # Setup the parts mapping
    $index = 0
    foreach($part in $allPathValues) {
        if(-not $actualValuePartsMapping.ContainsKey($part)) {
            $actualValuePartsMapping[$part] = $index
        }
        else {
            if($Reverse) {
                $actualValuePartsMapping[$part] = $index
            }
        }
        $index += 1
    }

    # Fill the indices to remove
    $indicesToRemove = [System.Collections.Generic.List[string]]::new()
    foreach($part in $ModuleValue.Split([IO.Path]::PathSeparator)) {
        if(-not $actualValuePartsMapping.ContainsKey($part)) {
            Write-Verbose "The PATH value '$part' is not part of the variable '$Variable' anymore"
            continue
        }

        $indicesToRemove.Add($actualValuePartsMapping[$part])
    }

    $indicesToRemove.Sort()
    $indicesToRemove.Reverse()

    foreach($index in $indicesToRemove) {
        $allPathValues.RemoveAt($index)
    }

    $newValue = ($allPathValues -join [IO.Path]::PathSeparator)
    [Environment]::SetEnvironmentVariable($Variable, $newValue, "Process")
}

function Remove-EnvironmentModuleFunction
{
    <#
    .SYNOPSIS
    Remove a function from the active environment stack.
    .DESCRIPTION
    This function will remove the given function from the active environment. The function is removed from the loaded functions stack.
    .PARAMETER FunctionDefinition
    The definition of the function.
    .OUTPUTS
    No output is returned.
    #>

    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    param (
        [EnvironmentModuleCore.FunctionInfo] $FunctionDefinition
    )

    # Check if the function was already used
    if($script:loadedEnvironmentModuleFunctions.ContainsKey($FunctionDefinition.Name))
    {
        $knownFunctions = $script:loadedEnvironmentModuleFunctions[$FunctionDefinition.Name]
        $knownFunctions.Remove($FunctionDefinition) | out-null
        if($knownFunctions.Count -eq 0) {
            $script:loadedEnvironmentModuleFunctions.Remove($FunctionDefinition.Name)
        }
    }
}

function Clear-EnvironmentModules([Switch] $Force)
{
    <#
    .SYNOPSIS
    Remove all loaded environment modules from the environment.
    .DESCRIPTION
    This function will remove all loaded environment modules, so that a clean environment module remains.
    .PARAMETER Force
    If this value is set, the user is not asked for module unload.
    .OUTPUTS
    No output is returned.
    #>


    $modules = Get-EnvironmentModule

    if($modules -and (-not $Force)) {
        $result = Show-ConfirmDialogue "Do you really want to remove all loaded environment modules?"
        if(-not $result) {
            return
        }
    }

    foreach($module in $modules) {
        if($module.IsLoadedDirectly) {
            Remove-EnvironmentModule $module.FullName -Force
        }
    }
}