| | 1 | | # DotEnv.psm1 |
| | 2 | |
|
| | 3 | | # Requires -Version 5.1 |
| | 4 | |
|
| | 5 | | using namespace System.IO |
| | 6 | | using namespace System.Management.Automation |
| | 7 | |
|
| 1 | 8 | | $script:trueOriginalEnvironmentVariables = @{} # Stores { VarName = OriginalValueOrNull } - a persistent record of pre-m |
| 1 | 9 | | $script:previousEnvFiles = @() |
| 1 | 10 | | $script:previousWorkingDirectory = $PWD.Path |
| 1 | 11 | | $script:e = [char]27 |
| 1 | 12 | | $script:itemiserA = [char]0x2022 |
| 1 | 13 | | $script:itemiser = [char]0x21B3 |
| 1 | 14 | | $script:boldOn = "$($script:e)[1m" |
| 1 | 15 | | $script:boldOff = "$($script:e)[0m" # Resets all attributes (color, bold, underline etc.) |
| | 16 | |
|
| | 17 | | # $DebugPreference = 'Continue' |
| | 18 | |
|
| | 19 | | function Get-RelativePath { |
| | 20 | | [CmdletBinding()] |
| | 21 | | param( |
| | 22 | | [Parameter(Mandatory)] |
| | 23 | | [string]$Path, |
| | 24 | |
|
| | 25 | | [Parameter(Mandatory)] |
| | 26 | | [string]$BasePath |
| | 27 | | ) |
| | 28 | |
|
| | 29 | | try { |
| 1 | 30 | | $absTarget = [System.IO.Path]::GetFullPath($Path) |
| 1 | 31 | | $absBase = [System.IO.Path]::GetFullPath($BasePath) |
| | 32 | |
|
| 1 | 33 | | if ($absTarget.Equals($absBase, [System.StringComparison]::OrdinalIgnoreCase)) { |
| 0 | 34 | | return "." |
| | 35 | | } |
| | 36 | |
|
| | 37 | | # Ensure BasePath for Uri ends with a directory separator. |
| 1 | 38 | | $uriBaseNormalized = $absBase |
| 1 | 39 | | if (-not $uriBaseNormalized.EndsWith([System.IO.Path]::DirectorySeparatorChar)) { |
| 1 | 40 | | $uriBaseNormalized += [System.IO.Path]::DirectorySeparatorChar |
| | 41 | | } |
| 1 | 42 | | $baseUri = [System.Uri]::new($uriBaseNormalized) |
| 1 | 43 | | $targetUri = [System.Uri]::new($absTarget) |
| | 44 | |
|
| 1 | 45 | | $relativeUri = $baseUri.MakeRelativeUri($targetUri) |
| 1 | 46 | | $relativePath = [System.Uri]::UnescapeDataString($relativeUri.ToString()) |
| | 47 | |
|
| 1 | 48 | | return $relativePath.Replace('/', [System.IO.Path]::DirectorySeparatorChar) |
| | 49 | | } |
| | 50 | | catch { |
| 0 | 51 | | Write-Warning "Get-RelativePath: Error calculating relative path for Target '$Path' from Base '$BasePath'. Error: $( |
| 0 | 52 | | return $Path |
| | 53 | | } |
| | 54 | | } |
| | 55 | |
|
| | 56 | | # Cannot be local as this is mocked in Import-DotEnv tests |
| | 57 | | function Get-EnvFilesUpstream { |
| | 58 | | [CmdletBinding()] |
| | 59 | | param([string]$Directory = ".") |
| | 60 | |
|
| | 61 | | try { |
| 1 | 62 | | $resolvedPath = Convert-Path -Path $Directory -ErrorAction Stop |
| | 63 | | } |
| | 64 | | catch { |
| 0 | 65 | | Write-Warning "Get-EnvFilesUpstream: Error resolving path '$Directory'. Error: $($_.Exception.Message). Defaulting t |
| 0 | 66 | | $resolvedPath = $PWD.Path |
| | 67 | | # Removed unused variable assignment for $currentDirNormalized |
| | 68 | | } |
| | 69 | |
|
| 1 | 70 | | $envFiles = [System.Collections.Generic.List[string]]::new() |
| 1 | 71 | | $currentSearchDir = $resolvedPath |
| | 72 | |
|
| 1 | 73 | | while ($currentSearchDir) { |
| 1 | 74 | | $envPath = Join-Path $currentSearchDir ".env" |
| 1 | 75 | | if (Test-Path -LiteralPath $envPath -PathType Leaf) { |
| 1 | 76 | | $envFiles.Add($envPath) |
| | 77 | | } |
| 1 | 78 | | $parentDir = Split-Path -Path $currentSearchDir -Parent |
| 1 | 79 | | if ($parentDir -eq $currentSearchDir -or [string]::IsNullOrEmpty($parentDir)) { break } |
| 1 | 80 | | $currentSearchDir = $parentDir |
| | 81 | | } |
| | 82 | |
|
| 1 | 83 | | if ($envFiles.Count -gt 0) { |
| 1 | 84 | | $envFiles.Reverse() |
| | 85 | | } |
| 1 | 86 | | return [string[]]$envFiles |
| | 87 | | } |
| | 88 | |
|
| | 89 | | function Format-EnvFilePath { |
| | 90 | | [CmdletBinding()] |
| | 91 | | param( |
| | 92 | | [Parameter(Mandatory)] |
| | 93 | | [string]$Path, |
| | 94 | |
|
| | 95 | | [Parameter(Mandatory)] |
| | 96 | | [string]$BasePath |
| | 97 | | ) |
| | 98 | |
|
| 1 | 99 | | $relativePath = Get-RelativePath -Path $Path -BasePath $BasePath |
| 1 | 100 | | $corePath = Split-Path -Path $relativePath -Parent |
| | 101 | |
|
| 1 | 102 | | if (-not [string]::IsNullOrEmpty($corePath)) { |
| 1 | 103 | | $boldCore = "${script:e}[1m${corePath}${script:e}[22m" |
| 1 | 104 | | $relativePath = $relativePath.Replace($corePath, $boldCore) |
| | 105 | | } |
| | 106 | |
|
| 1 | 107 | | return $relativePath |
| | 108 | | } |
| | 109 | |
|
| | 110 | | function Format-VarHyperlink { |
| | 111 | | param( |
| | 112 | | [string]$VarName, |
| | 113 | | [string]$FilePath, |
| | 114 | | [int]$LineNumber |
| | 115 | | ) |
| | 116 | | # Ensure FilePath is absolute for the hyperlink |
| 1 | 117 | | $absFilePath = try { Resolve-Path -LiteralPath $FilePath -ErrorAction Stop } catch { $FilePath } |
| 1 | 118 | | $fileUrl = "vscode://file/$($absFilePath):${LineNumber}" |
| 1 | 119 | | return "$script:e]8;;$fileUrl$script:e\$VarName$script:e]8;;$script:e\" |
| | 120 | | } |
| | 121 | |
|
| | 122 | | # --- Helper function to get effective environment variables from a list of .env files --- |
| | 123 | | function Get-EnvVarsFromFiles { |
| | 124 | | param( |
| | 125 | | [string[]]$Files, |
| | 126 | | [string]$BasePath # BasePath is for context, not directly used in var aggregation here |
| | 127 | | ) |
| | 128 | |
|
| | 129 | | function Read-EnvFile { |
| | 130 | | param([string]$FilePath) |
| 1 | 131 | | $vars = @{} |
| 1 | 132 | | if (-not ([System.IO.File]::Exists($FilePath))) { |
| 1 | 133 | | Write-Debug "Parse-EnvFile: File '$FilePath' does not exist." |
| 1 | 134 | | return $vars |
| | 135 | | } |
| | 136 | | try { |
| 1 | 137 | | $lines = [System.IO.File]::ReadLines($FilePath) |
| | 138 | | } catch { |
| 0 | 139 | | Write-Warning "Parse-EnvFile: Error reading file '$FilePath'. Error: $($_.Exception.Message)" |
| 0 | 140 | | return $vars |
| | 141 | | } |
| 1 | 142 | | $lineNumber = 0 |
| 1 | 143 | | foreach ($line in $lines) { |
| 1 | 144 | | $lineNumber++ |
| 1 | 145 | | if ([string]::IsNullOrWhiteSpace($line)) { continue } |
| 1 | 146 | | $trimmed = $line.TrimStart() |
| 1 | 147 | | if ($trimmed.StartsWith('#')) { continue } |
| 1 | 148 | | $split = $line.Split('=', 2) |
| 1 | 149 | | if ($split.Count -eq 2) { |
| 1 | 150 | | $varName = $split[0].Trim() |
| 1 | 151 | | $varValue = $split[1].Trim() |
| 1 | 152 | | $vars[$varName] = @{ Value = $varValue; Line = $lineNumber; SourceFile = $FilePath } |
| | 153 | | } |
| | 154 | | } |
| 1 | 155 | | return $vars |
| | 156 | | } |
| | 157 | |
|
| 1 | 158 | | if ($Files.Count -eq 0) { |
| 1 | 159 | | return @{} |
| | 160 | | } |
| | 161 | |
|
| 1 | 162 | | if ($Files.Count -eq 1) { |
| | 163 | | # Fast path for a single file. Parse-EnvFile returns the rich structure. |
| 1 | 164 | | return Read-EnvFile -FilePath $Files[0] |
| | 165 | | } |
| | 166 | |
|
| | 167 | | # For multiple files, use RunspacePool for parallel parsing. |
| 1 | 168 | | $finalEffectiveVars = @{} |
| 1 | 169 | | $parsedResults = New-Object "object[]" $Files.Count # To store results in order |
| | 170 | |
|
| | 171 | | # Define the script that will be run in each runspace. |
| | 172 | | # It includes a minimal Parse-EnvFile definition to ensure it's available and self-contained. |
| 1 | 173 | | $scriptBlockText = @' |
| | 174 | | param([string]$PathToParse) |
| | 175 | |
|
| | 176 | | # Minimal Parse-EnvFile definition for use in isolated runspaces |
| | 177 | | function Parse-EnvFileInRunspace { |
| | 178 | | param([string]$LocalFilePath) |
| | 179 | | $localVars = @{} # PowerShell hashtable literal is fine here, it's a PS runspace |
| | 180 | | # Directly use System.IO.File for existence and reading to minimize dependencies |
| | 181 | | if (-not ([System.IO.File]::Exists($LocalFilePath))) { |
| | 182 | | return $localVars |
| | 183 | | } |
| | 184 | | try { |
| | 185 | | $fileLines = [System.IO.File]::ReadLines($LocalFilePath) |
| | 186 | | } catch { |
| | 187 | | # Silently return empty on read error in this isolated context |
| | 188 | | return $localVars |
| | 189 | | } |
| | 190 | | $lineNum = 0 |
| | 191 | | foreach ($txtLine in $fileLines) { |
| | 192 | | $lineNum++ |
| | 193 | | if ([string]::IsNullOrWhiteSpace($txtLine)) { continue } |
| | 194 | | $trimmedTxtLine = $txtLine.TrimStart() |
| | 195 | | if ($trimmedTxtLine.StartsWith('#')) { continue } |
| | 196 | | $parts = $txtLine.Split('=', 2) |
| | 197 | | if ($parts.Count -eq 2) { |
| | 198 | | $name = $parts[0].Trim() |
| | 199 | | $val = $parts[1].Trim() |
| | 200 | | # This structure needs to match what the rest of the module expects |
| | 201 | | $localVars[$name] = @{ Value = $val; Line = $lineNum; SourceFile = $LocalFilePath } |
| | 202 | | } |
| | 203 | | } |
| | 204 | | return $localVars |
| | 205 | | } |
| | 206 | |
|
| | 207 | | Parse-EnvFileInRunspace -LocalFilePath $PathToParse |
| | 208 | | '@ |
| | 209 | |
|
| | 210 | | # Determine a reasonable number of runspaces. Cap at 8 to avoid excessive resource use. |
| | 211 | | # Fix: [Math]::Min takes only two arguments. Nest calls for three values. |
| 1 | 212 | | $maxRunspaces = [Math]::Min(8, [Math]::Min($Files.Count, ([System.Environment]::ProcessorCount * 2))) |
| 1 | 213 | | $minRunspaces = 1 |
| | 214 | |
|
| 1 | 215 | | $iss = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault2() |
| | 216 | | # CreateDefault2 is generally good for providing access to common .NET types like System.IO.File |
| | 217 | |
|
| 1 | 218 | | $runspacePool = $null |
| 1 | 219 | | $psInstanceTrackers = [System.Collections.Generic.List[object]]::new() |
| | 220 | |
|
| | 221 | | try { |
| 1 | 222 | | $runspacePool = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspacePool($minRunspaces, $max |
| 1 | 223 | | $runspacePool.Open() |
| | 224 | |
|
| 1 | 225 | | for ($i = 0; $i -lt $Files.Count; $i++) { |
| 1 | 226 | | $fileToParse = $Files[$i] |
| 1 | 227 | | $ps = [PowerShell]::Create() |
| 1 | 228 | | $ps.RunspacePool = $runspacePool |
| 1 | 229 | | $null = $ps.AddScript($scriptBlockText).AddArgument($fileToParse) |
| | 230 | |
|
| 1 | 231 | | $asyncResult = $ps.BeginInvoke() |
| 1 | 232 | | $psInstanceTrackers.Add([PSCustomObject]@{ |
| 1 | 233 | | PowerShell = $ps |
| 1 | 234 | | AsyncResult = $asyncResult |
| 1 | 235 | | OriginalIndex = $i |
| 1 | 236 | | FilePath = $fileToParse # For logging/debugging |
| | 237 | | }) |
| | 238 | | } |
| | 239 | |
|
| | 240 | | # Wait for all to complete and collect results |
| 1 | 241 | | foreach ($tracker in $psInstanceTrackers) { |
| | 242 | | try { |
| 1 | 243 | | $outputCollection = $tracker.PowerShell.EndInvoke($tracker.AsyncResult) |
| | 244 | |
|
| 1 | 245 | | if ($tracker.PowerShell.Streams.Error.Count -gt 0) { |
| 0 | 246 | | foreach($err in $tracker.PowerShell.Streams.Error){ |
| 0 | 247 | | Write-Warning "Error parsing file '$($tracker.FilePath)' in parallel: $($err.ToString())" |
| | 248 | | } |
| 0 | 249 | | $parsedResults[$tracker.OriginalIndex] = @{} |
| 1 | 250 | | } elseif ($null -ne $outputCollection -and $outputCollection.Count -eq 1) { |
| 1 | 251 | | $singleOutput = $outputCollection[0] |
| 1 | 252 | | if ($singleOutput -is [System.Collections.IDictionary]) { # Directly a hashtable |
| 1 | 253 | | $parsedResults[$tracker.OriginalIndex] = $singleOutput |
| 0 | 254 | | } elseif ($singleOutput -is [System.Management.Automation.PSObject] -and $singleOutput.BaseObject -i |
| 0 | 255 | | $parsedResults[$tracker.OriginalIndex] = $singleOutput.BaseObject |
| | 256 | | } else { |
| 0 | 257 | | Write-Warning "Unexpected output type from parallel parsing of '$($tracker.FilePath)'. Type: $($ |
| 0 | 258 | | $parsedResults[$tracker.OriginalIndex] = @{} |
| | 259 | | } |
| | 260 | | } else { |
| 0 | 261 | | Write-Warning "No output or multiple outputs from parallel parsing of '$($tracker.FilePath)'. Output |
| 0 | 262 | | $parsedResults[$tracker.OriginalIndex] = @{} |
| | 263 | | } |
| | 264 | | } catch { |
| 0 | 265 | | Write-Warning "Exception during EndInvoke for file '$($tracker.FilePath)': $($_.Exception.Message)" |
| 0 | 266 | | $parsedResults[$tracker.OriginalIndex] = @{} # Store empty on exception |
| | 267 | | } |
| | 268 | | } |
| | 269 | | } |
| | 270 | | finally { |
| 1 | 271 | | foreach ($tracker in $psInstanceTrackers) { |
| 1 | 272 | | if ($tracker.PowerShell) { |
| 1 | 273 | | $tracker.PowerShell.Dispose() |
| | 274 | | } |
| | 275 | | } |
| 1 | 276 | | if ($runspacePool) { |
| 1 | 277 | | $runspacePool.Close() |
| 1 | 278 | | $runspacePool.Dispose() |
| | 279 | | } |
| | 280 | | } |
| | 281 | |
|
| | 282 | | # Sequentially merge the parsed results to ensure correct precedence. |
| 1 | 283 | | foreach ($fileScopedVarsHashtable in $parsedResults) { |
| 1 | 284 | | if ($null -eq $fileScopedVarsHashtable) { continue } # Skip if null (e.g. error during parsing) |
| 1 | 285 | | foreach ($varNameKey in $fileScopedVarsHashtable.Keys) { |
| 1 | 286 | | $finalEffectiveVars[$varNameKey] = $fileScopedVarsHashtable[$varNameKey] |
| | 287 | | } |
| | 288 | | } |
| 1 | 289 | | return $finalEffectiveVars |
| | 290 | | } |
| | 291 | |
|
| | 292 | | function Import-DotEnv { |
| | 293 | | [CmdletBinding(DefaultParameterSetName = 'Load', HelpUri = 'https://github.com/CosmicDNA/ImportDotEnv#readme')] |
| | 294 | | param( |
| | 295 | | [Parameter(ParameterSetName = 'Load', Position = 0, ValueFromPipelineByPropertyName = $true)] |
| | 296 | | [string]$Path, |
| | 297 | |
|
| | 298 | | [Parameter(ParameterSetName = 'Unload')] |
| | 299 | | [switch]$Unload, |
| | 300 | |
|
| | 301 | | [Parameter(ParameterSetName = 'Help')] |
| | 302 | | [switch]$Help, |
| | 303 | |
|
| | 304 | | [Parameter(ParameterSetName = 'List')] |
| | 305 | | [switch]$List |
| | 306 | | ) |
| | 307 | |
|
| | 308 | | # --- Helper: Parse a single .env line into [name, value] or $null --- |
| | 309 | | function Convert-EnvLine { |
| | 310 | | param([string]$Line) |
| 1 | 311 | | if ([string]::IsNullOrWhiteSpace($Line)) { return $null } |
| 1 | 312 | | $trimmed = $Line.TrimStart() |
| 1 | 313 | | if ($trimmed.StartsWith('#')) { return $null } |
| 1 | 314 | | $split = $Line.Split('=', 2) |
| 1 | 315 | | if ($split.Count -eq 2) { |
| 1 | 316 | | return @($split[0].Trim(), $split[1].Trim()) |
| | 317 | | } |
| 0 | 318 | | return $null |
| | 319 | | } |
| | 320 | |
|
| | 321 | | function Get-VarsToRestoreByFileMap { |
| | 322 | | param( |
| | 323 | | [string[]]$Files, |
| | 324 | | [string[]]$VarsToRestore |
| | 325 | | ) |
| | 326 | |
|
| | 327 | | function Get-EnvVarNamesFromFile { |
| | 328 | | param([string]$FilePath) |
| 1 | 329 | | if (-not (Test-Path -LiteralPath $FilePath -PathType Leaf)) { return @() } |
| | 330 | | try { |
| 1 | 331 | | return [System.IO.File]::ReadLines($FilePath) | ForEach-Object { |
| 1 | 332 | | $parsed = Convert-EnvLine $_ |
| 1 | 333 | | if ($null -ne $parsed) { $parsed[0] } |
| 1 | 334 | | } | Where-Object { $_ } |
| | 335 | | } catch { |
| 0 | 336 | | Write-Warning "Get-EnvVarNamesFromFile: Error reading file '$FilePath'. Skipping. Error: $($_.Exception.Message) |
| 0 | 337 | | return @() |
| | 338 | | } |
| | 339 | | } |
| | 340 | |
|
| 1 | 341 | | $varsToUnsetByFileMap = @{} |
| 1 | 342 | | foreach ($fileToScan in $Files) { |
| 1 | 343 | | foreach ($parsedVarName in Get-EnvVarNamesFromFile -FilePath $fileToScan) { |
| 1 | 344 | | if ($VarsToRestore -contains $parsedVarName) { |
| 1 | 345 | | if (-not $varsToUnsetByFileMap.ContainsKey($fileToScan)) { $varsToUnsetByFileMap[$fileToScan] = [System.Collec |
| 1 | 346 | | $varsToUnsetByFileMap[$fileToScan].Add($parsedVarName) |
| | 347 | | } |
| | 348 | | } |
| | 349 | | } |
| 1 | 350 | | return $varsToUnsetByFileMap |
| | 351 | | } |
| | 352 | |
|
| 1 | 353 | | if ($PSCmdlet.ParameterSetName -eq 'Unload') { |
| 1 | 354 | | Write-Debug "MODULE Import-DotEnv: Called with -Unload switch." |
| 1 | 355 | | $varsFromLastLoad = Get-EnvVarsFromFiles -Files $script:previousEnvFiles -BasePath $script:previousWorkingDirectory |
| | 356 | |
|
| 1 | 357 | | if ($varsFromLastLoad.Count -gt 0) { |
| 1 | 358 | | Write-Host "`nUnloading active .env configuration(s)..." -ForegroundColor Yellow |
| | 359 | |
|
| 1 | 360 | | $allVarsToRestore = $varsFromLastLoad.Keys |
| 1 | 361 | | $varsToRestoreByFileMap = Get-VarsToRestoreByFileMap -Files $script:previousEnvFiles -VarsToRestore $allVarsToRest |
| | 362 | |
|
| 1 | 363 | | $varsCoveredByFileMap = $varsToRestoreByFileMap.Values | ForEach-Object { $_ } | Sort-Object -Unique |
| 1 | 364 | | $varsToRestoreNoFileAssociation = $allVarsToRestore | Where-Object { $varsCoveredByFileMap -notcontains $_ } |
| | 365 | |
|
| 1 | 366 | | Restore-EnvVars -VarsToRestoreByFileMap $varsToRestoreByFileMap -VarNames $varsToRestoreNoFileAssociation -TrueOri |
| | 367 | |
|
| 1 | 368 | | $script:previousEnvFiles = @() |
| 1 | 369 | | $script:previousWorkingDirectory = "STATE_AFTER_EXPLICIT_UNLOAD" |
| 1 | 370 | | Write-Host "Environment restored. Module state reset." -ForegroundColor Green |
| | 371 | | } |
| | 372 | | return |
| | 373 | | } |
| | 374 | |
|
| 1 | 375 | | if ($PSCmdlet.ParameterSetName -eq 'Help' -or $Help) { |
| 0 | 376 | | Write-Host @" |
| | 377 | |
|
| | 378 | | `e[1mImport-DotEnv Module Help`e[0m |
| | 379 | |
|
| | 380 | | This module allows for hierarchical loading and unloading of .env files. |
| | 381 | | It also provides integration with `Set-Location` (cd/sl) to automatically |
| | 382 | | manage environment variables as you navigate directories. |
| | 383 | |
|
| | 384 | | `e[1mUsage:`e[0m |
| | 385 | |
|
| | 386 | | `e[1mImport-DotEnv`e[0m [-Path <string>] |
| | 387 | | Loads .env files from the specified path (or current directory if no path given) |
| | 388 | | and its parent directories. Variables from deeper .env files take precedence. |
| | 389 | | Automatically unloads variables from previously loaded .env files if they are |
| | 390 | | no longer applicable or have changed. |
| | 391 | |
|
| | 392 | | `e[1mImport-DotEnv -Unload`e[0m |
| | 393 | | Unloads all variables set by the module and resets its internal state. |
| | 394 | |
|
| | 395 | | `e[1mImport-DotEnv -List`e[0m |
| | 396 | | Lists currently active variables and the .env files defining them. |
| | 397 | |
|
| | 398 | | `e[1mImport-DotEnv -Help`e[0m |
| | 399 | | Displays this help message. |
| | 400 | |
|
| | 401 | | For `Set-Location` integration, use `Enable-ImportDotEnvCdIntegration` and `Disable-ImportDotEnvCdIntegration`. |
| | 402 | | "@ |
| | 403 | | return |
| | 404 | | } |
| | 405 | |
|
| 1 | 406 | | if ($PSCmdlet.ParameterSetName -eq 'List') { |
| 1 | 407 | | Write-Debug "MODULE Import-DotEnv: Called with -List switch." |
| 1 | 408 | | if (-not $script:previousEnvFiles -or $script:previousEnvFiles.Count -eq 0 -or $script:previousWorkingDirectory -eq |
| 1 | 409 | | Write-Host "No .env configuration is currently active or managed by ImportDotEnv." -ForegroundColor Magenta |
| | 410 | | return |
| | 411 | | } |
| 1 | 412 | | $effectiveVars = Get-EnvVarsFromFiles -Files $script:previousEnvFiles -BasePath $script:previousWorkingDirectory |
| | 413 | | function Get-VarToFilesMap($files) { |
| 1 | 414 | | $map = @{} |
| 1 | 415 | | foreach ($file in $files) { |
| 1 | 416 | | if (Test-Path -LiteralPath $file -PathType Leaf) { |
| 1 | 417 | | foreach ($line in [System.IO.File]::ReadLines($file)) { |
| 1 | 418 | | $parsed = Convert-EnvLine $line |
| 1 | 419 | | if ($parsed) { |
| 1 | 420 | | $var = $parsed[0] |
| 1 | 421 | | if (-not $map[$var]) { $map[$var] = @() } |
| 1 | 422 | | $map[$var] += $file |
| | 423 | | } |
| | 424 | | } |
| | 425 | | } |
| | 426 | | } |
| 1 | 427 | | $map |
| | 428 | | } |
| 1 | 429 | | $varToFiles = Get-VarToFilesMap $script:previousEnvFiles |
| 1 | 430 | | $outputObjects = $effectiveVars.Keys | Sort-Object | ForEach-Object { |
| 1 | 431 | | $var = $_ |
| 1 | 432 | | $varPlainName = $var # Store plain name for calculations |
| 1 | 433 | | $effectiveVarDetail = $effectiveVars[$var] # Get details of the effective variable (SourceFile, Line) |
| 1 | 434 | | $hyperlinkedName = Format-VarHyperlink -VarName $varPlainName -FilePath $effectiveVarDetail.SourceFile -LineNumber |
| | 435 | |
|
| | 436 | | # For 'Defined In', list all files where the variable name appears |
| 1 | 437 | | $definingFilesPaths = $varToFiles[$var] # This is an array of file paths from Get-VarToFilesMap |
| 1 | 438 | | $definedInDisplay = ($definingFilesPaths | ForEach-Object { " $(Get-RelativePath -Path $_ -BasePath $PWD.Path)" } |
| | 439 | |
|
| 1 | 440 | | [PSCustomObject]@{ |
| 1 | 441 | | NameForOutput = $hyperlinkedName # Always the hyperlinked version |
| 1 | 442 | | NamePlainForCalc = $varPlainName # Always the plain version, for calculations |
| 1 | 443 | | 'Defined In' = $definedInDisplay |
| | 444 | | } |
| | 445 | | } |
| 1 | 446 | | if ($outputObjects) { |
| 1 | 447 | | if ($PSVersionTable.PSVersion.Major -ge 7) { |
| | 448 | | # For PS7+, use NameForOutput (which has hyperlink), Format-Table handles ANSI well. |
| | 449 | | # Ensure the column header is "Name". |
| 1 | 450 | | $outputObjects | Format-Table -Property @{Expression={$_.NameForOutput}; Label="Name"}, 'Defined In' -AutoSize |
| | 451 | | } else { |
| | 452 | | # PS5.1: Manual formatting to try and preserve hyperlinks while maintaining table structure. |
| | 453 | | # This works best in terminals that understand ANSI hyperlinks (like Windows Terminal running PS5.1). |
| | 454 | | # In older conhost.exe, ANSI codes might print literally. |
| 1 | 455 | | $maxPlainNameLength = 0 |
| 1 | 456 | | $nameLengths = $outputObjects | ForEach-Object { $_.NamePlainForCalc.Length } |
| 1 | 457 | | if ($nameLengths) { |
| 1 | 458 | | $maxPlainNameLength = ($nameLengths | Measure-Object -Maximum).Maximum |
| | 459 | | } |
| | 460 | | # Ensure $nameColPaddedWidth is a clean integer for use in format strings. |
| 1 | 461 | | $nameColPaddedWidth = [int]([Math]::Max("Name".Length, $maxPlainNameLength)) |
| | 462 | |
|
| 1 | 463 | | $nameHeaderTextPlain = "Name" |
| 1 | 464 | | $definedInHeaderTextPlain = "Defined In" |
| | 465 | |
|
| 1 | 466 | | $nameHeaderFormatted = "$($script:boldOn)${nameHeaderTextPlain}$($script:boldOff)" |
| 1 | 467 | | $definedInHeaderFormatted = "$($script:boldOn)${definedInHeaderTextPlain}$($script:boldOff)" |
| | 468 | |
|
| 1 | 469 | | Write-Host "" |
| | 470 | | # --- Print Header Titles --- |
| 1 | 471 | | Write-Host -NoNewline $nameHeaderFormatted -ForegroundColor Green |
| | 472 | | # Calculate padding based on the plain text length of the "Name" header |
| 1 | 473 | | $paddingForNameHeader = [Math]::Max(0, $nameColPaddedWidth - $nameHeaderTextPlain.Length) |
| 1 | 474 | | Write-Host -NoNewline (" " * $paddingForNameHeader) |
| 1 | 475 | | Write-Host -NoNewline " " # Column separator |
| 1 | 476 | | Write-Host $definedInHeaderFormatted -ForegroundColor Green |
| | 477 | |
|
| | 478 | | # --- Print Header Underlines --- |
| 1 | 479 | | $nameUnderline = "-" * $nameColPaddedWidth # Underline spans the full calculated width of the first column |
| 1 | 480 | | $definedInUnderline = "-" * $definedInHeaderTextPlain.Length # Underline matches the visible text of "Defined In |
| 1 | 481 | | Write-Host -NoNewline $nameUnderline -ForegroundColor Green |
| 1 | 482 | | Write-Host -NoNewline " " # Column separator |
| 1 | 483 | | Write-Host $definedInUnderline -ForegroundColor Green |
| | 484 | |
|
| 1 | 485 | | foreach ($obj in $outputObjects) { |
| 1 | 486 | | $nameToPrint = $obj.NameForOutput # This is the hyperlink string |
| 1 | 487 | | $plainNameActualLength = $obj.NamePlainForCalc.Length # Calculate actual length of the plain name |
| 1 | 488 | | $definedInLines = $obj.'Defined In' -split [Environment]::NewLine |
| | 489 | |
|
| 1 | 490 | | Write-Host -NoNewline $nameToPrint |
| 1 | 491 | | $spacesNeededAfterName = [Math]::Max(0, $nameColPaddedWidth - $plainNameActualLength) # Use calculated plain |
| 1 | 492 | | Write-Host -NoNewline (" " * $spacesNeededAfterName) |
| 1 | 493 | | Write-Host -NoNewline " " # Column separator |
| 1 | 494 | | Write-Host $definedInLines[0] # First line of "Defined In" |
| | 495 | | # Subsequent lines of "Defined In", correctly indented |
| 1 | 496 | | for ($j = 1; $j -lt $definedInLines.Length; $j++) { |
| 1 | 497 | | Write-Host (" " * ($nameColPaddedWidth + 2)) $definedInLines[$j] # Indent under "Defined In" |
| | 498 | | } |
| | 499 | | } |
| 1 | 500 | | Write-Host "" |
| | 501 | | } |
| | 502 | | } else { |
| 0 | 503 | | Write-Host "No effective variables found in the active configuration." -ForegroundColor Yellow |
| | 504 | | } |
| | 505 | | return |
| | 506 | | } |
| | 507 | |
|
| | 508 | | # --- Load Parameter Set Logic (Default) --- |
| 1 | 509 | | Write-Debug "MODULE Import-DotEnv: Called with Path '$Path' (Load set). Current PWD: $($PWD.Path)" |
| 1 | 510 | | if ($PSCmdlet.ParameterSetName -eq 'Load' -and (-not $PSBoundParameters.ContainsKey('Path'))) { |
| 0 | 511 | | $Path = "." |
| | 512 | | } |
| | 513 | | try { |
| 1 | 514 | | $resolvedPath = Convert-Path -Path $Path -ErrorAction Stop |
| | 515 | | } catch { |
| 0 | 516 | | $resolvedPath = $PWD.Path |
| 0 | 517 | | Write-Warning "Import-DotEnv: The specified path '$Path' could not be resolved. Falling back to current directory: ' |
| 0 | 518 | | Write-Debug "MODULE Import-DotEnv: Path '$Path' resolved to PWD '$resolvedPath' due to error: $($_.Exception.Message |
| | 519 | | } |
| | 520 | |
|
| 1 | 521 | | $currentEnvFiles = Get-EnvFilesUpstream -Directory $resolvedPath |
| 1 | 522 | | Write-Debug "MODULE Import-DotEnv: Resolved path '$resolvedPath'. Found $($currentEnvFiles.Count) .env files upstream: |
| 1 | 523 | | Write-Debug "MODULE Import-DotEnv: Previous files count: $($script:previousEnvFiles.Count) ('$($script:previousEnvFile |
| | 524 | |
|
| 1 | 525 | | $prevVars = Get-EnvVarsFromFiles -Files $script:previousEnvFiles -BasePath $script:previousWorkingDirectory |
| 1 | 526 | | $currVars = Get-EnvVarsFromFiles -Files $currentEnvFiles -BasePath $resolvedPath |
| | 527 | |
|
| | 528 | | # --- Unload Phase: Unset variables that were in prevVars but not in currVars, or if their value changed --- |
| 1 | 529 | | $varsToUnsetOrRestore = @() |
| 1 | 530 | | foreach ($varNameKey in $prevVars.Keys) { |
| 1 | 531 | | if (-not $currVars.ContainsKey($varNameKey) -or $currVars[$varNameKey].Value -ne $prevVars[$varNameKey].Value) { |
| 1 | 532 | | $varsToUnsetOrRestore += $varNameKey |
| | 533 | | } |
| | 534 | | } |
| | 535 | |
|
| 1 | 536 | | if ($varsToUnsetOrRestore.Count -gt 0) { |
| 1 | 537 | | $varsToRestoreByFileMap = Get-VarsToRestoreByFileMap -Files $script:previousEnvFiles -VarsToRestore $varsToUnsetOrRe |
| 1 | 538 | | $varsCoveredByFileMap = $varsToRestoreByFileMap.Values | ForEach-Object { $_ } | Sort-Object -Unique |
| 1 | 539 | | $varsToRestoreNoFileAssociation = $varsToUnsetOrRestore | Where-Object { $varsCoveredByFileMap -notcontains $_ } |
| 1 | 540 | | Restore-EnvVars -VarsToRestoreByFileMap $varsToRestoreByFileMap -VarNames $varsToRestoreNoFileAssociation -TrueOrigi |
| | 541 | | } |
| | 542 | |
|
| | 543 | | # --- Load Phase --- |
| 1 | 544 | | if ($currentEnvFiles.Count -gt 0) { |
| 1 | 545 | | foreach ($varNameKey in $currVars.Keys) { |
| 1 | 546 | | if (-not $script:trueOriginalEnvironmentVariables.ContainsKey($varNameKey)) { |
| 1 | 547 | | $currentEnvValue = [Environment]::GetEnvironmentVariable($varNameKey, 'Process') |
| 1 | 548 | | if (-not (Test-Path "Env:\$varNameKey")) { |
| 1 | 549 | | $script:trueOriginalEnvironmentVariables[$varNameKey] = $null |
| | 550 | | } else { |
| 1 | 551 | | $script:trueOriginalEnvironmentVariables[$varNameKey] = $currentEnvValue |
| | 552 | | } |
| | 553 | | } |
| | 554 | | } |
| | 555 | |
|
| 1 | 556 | | $varsToReportAsSetOrChanged = [System.Collections.Generic.List[PSCustomObject]]::new() # Changed to PSCustomObject |
| 1 | 557 | | foreach ($varNameKey in $currVars.Keys) { |
| 1 | 558 | | $desiredVarInfo = $currVars[$varNameKey] |
| 1 | 559 | | $desiredValue = $desiredVarInfo.Value |
| 1 | 560 | | $currentValue = [Environment]::GetEnvironmentVariable($varNameKey, 'Process') |
| | 561 | | # Fix: Correctly set empty string as value, not as $null (which unsets) |
| 1 | 562 | | if ($currentValue -ne $desiredValue) { |
| 1 | 563 | | if ($null -eq $desiredValue) { |
| 0 | 564 | | [Environment]::SetEnvironmentVariable($varNameKey, $null) |
| | 565 | | } else { |
| 1 | 566 | | [Environment]::SetEnvironmentVariable($varNameKey, $desiredValue) |
| | 567 | | } |
| | 568 | | } |
| 1 | 569 | | $isNewToSession = (-not $prevVars.ContainsKey($varNameKey)) |
| 1 | 570 | | $hasValueChanged = $false |
| 1 | 571 | | if (-not $isNewToSession -and $prevVars[$varNameKey].Value -ne $desiredValue) { |
| 1 | 572 | | $hasValueChanged = $true |
| | 573 | | } |
| 1 | 574 | | Write-Verbose "Var: '$varNameKey', IsNew: $isNewToSession, HasChanged: $hasValueChanged" |
| 1 | 575 | | if (-not $isNewToSession) { |
| 1 | 576 | | Write-Verbose " PrevValue: '$($prevVars[$varNameKey].Value)', DesiredValue: '$desiredValue'" |
| | 577 | | } |
| | 578 | |
|
| 1 | 579 | | if ($isNewToSession -or $hasValueChanged) { |
| 1 | 580 | | $varsToReportAsSetOrChanged.Add([PSCustomObject]@{ # Changed to PSCustomObject |
| 1 | 581 | | Name = $varNameKey |
| 1 | 582 | | Line = $desiredVarInfo.Line |
| 1 | 583 | | SourceFile = $desiredVarInfo.SourceFile |
| | 584 | | }) |
| | 585 | | } |
| | 586 | | } |
| | 587 | |
|
| 1 | 588 | | if ($varsToReportAsSetOrChanged.Count -gt 0) { |
| 1 | 589 | | $groupedBySourceFile = $varsToReportAsSetOrChanged | Group-Object -Property SourceFile |
| 1 | 590 | | foreach ($fileGroup in $groupedBySourceFile) { |
| 1 | 591 | | $sourceFilePath = $fileGroup.Name # This is "" in PS5.1 if SourceFile was $null, and $null in PS7+ |
| | 592 | |
|
| | 593 | | # If SourceFile was $null (or missing), its group name might be $null or "" |
| | 594 | | # Skip processing for such groups as they don't represent a valid file path. |
| 1 | 595 | | if ([string]::IsNullOrEmpty($sourceFilePath)) { |
| 0 | 596 | | Write-Debug "Skipping report for variables with no valid SourceFile (group name was '$sourceFilePath')." |
| | 597 | | continue |
| | 598 | | } |
| | 599 | |
|
| 1 | 600 | | $formattedPath = Format-EnvFilePath -Path $sourceFilePath -BasePath $script:previousWorkingDirectory # Now $sour |
| 1 | 601 | | Write-Host "$script:itemiserA Processing .env file ${formattedPath}:" -ForegroundColor Cyan |
| 1 | 602 | | foreach ($varDetail in $fileGroup.Group) { |
| 1 | 603 | | $hyperlink = Format-VarHyperlink -VarName $varDetail.Name -FilePath $varDetail.SourceFile -LineNumber $varDeta |
| 1 | 604 | | Write-Host " $script:itemiser Setting environment variable: " -NoNewline |
| 1 | 605 | | Write-Host $hyperlink -ForegroundColor Green -NoNewline |
| 1 | 606 | | Write-Host " (from line $($varDetail.Line))" |
| | 607 | | } |
| | 608 | | } |
| | 609 | | } |
| | 610 | | } |
| | 611 | |
|
| 1 | 612 | | $script:previousEnvFiles = $currentEnvFiles |
| 1 | 613 | | $script:previousWorkingDirectory = $resolvedPath |
| | 614 | | } |
| | 615 | |
|
| | 616 | | # This function will be the wrapper for Set-Location |
| | 617 | | function Invoke-ImportDotEnvSetLocationWrapper { |
| | 618 | | [CmdletBinding(DefaultParameterSetName = 'Path', SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] |
| | 619 | | param( |
| | 620 | | [Parameter(ParameterSetName = 'Path', Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)] |
| | 621 | | [string]$Path, |
| | 622 | | [Parameter(ParameterSetName = 'LiteralPath', Mandatory, ValueFromPipelineByPropertyName)] |
| | 623 | | [Alias('PSPath')] |
| | 624 | | [string]$LiteralPath, |
| | 625 | | [Parameter()] |
| | 626 | | [switch]$PassThru, |
| | 627 | | [Parameter()] |
| | 628 | | [string]$StackName |
| | 629 | | ) |
| | 630 | |
|
| 1 | 631 | | $slArgs = @{} |
| 1 | 632 | | if ($PSCmdlet.ParameterSetName -eq 'Path') { |
| 1 | 633 | | if ($PSBoundParameters.ContainsKey('Path')) { $slArgs.Path = $Path } |
| 0 | 634 | | } elseif ($PSCmdlet.ParameterSetName -eq 'LiteralPath') { |
| 0 | 635 | | $slArgs.LiteralPath = $LiteralPath |
| | 636 | | } |
| 1 | 637 | | if ($PSBoundParameters.ContainsKey('PassThru')) { $slArgs.PassThru = $PassThru } |
| 1 | 638 | | if ($PSBoundParameters.ContainsKey('StackName')) { $slArgs.StackName = $StackName } |
| | 639 | |
|
| 1 | 640 | | $CommonParameters = @('Verbose', 'Debug', 'ErrorAction', 'ErrorVariable', 'WarningAction', 'WarningVariable', |
| | 641 | | 'OutBuffer', 'OutVariable', 'PipelineVariable', 'InformationAction', 'InformationVariable', 'WhatIf', 'Confirm') |
| 1 | 642 | | foreach ($commonParam in $CommonParameters) { |
| 1 | 643 | | if ($PSBoundParameters.ContainsKey($commonParam)) { |
| 0 | 644 | | $slArgs[$commonParam] = $PSBoundParameters[$commonParam] |
| | 645 | | } |
| | 646 | | } |
| | 647 | |
|
| 1 | 648 | | Microsoft.PowerShell.Management\Set-Location @slArgs |
| 1 | 649 | | Import-DotEnv -Path $PWD.Path |
| | 650 | | } |
| | 651 | |
|
| | 652 | | function Enable-ImportDotEnvCdIntegration { |
| | 653 | | [CmdletBinding()] |
| | 654 | | param() |
| 1 | 655 | | $currentModuleForEnable = $MyInvocation.MyCommand.Module |
| 1 | 656 | | if (-not $currentModuleForEnable) { |
| 0 | 657 | | Write-Error "Enable-ImportDotEnvCdIntegration: Module context not found." -ErrorAction Stop |
| | 658 | | } |
| 1 | 659 | | if (-not $currentModuleForEnable.ExportedCommands.ContainsKey('Invoke-ImportDotEnvSetLocationWrapper')) { |
| 0 | 660 | | Write-Error "Enable-ImportDotEnvCdIntegration: Required wrapper 'Invoke-ImportDotEnvSetLocationWrapper' is not expor |
| | 661 | | } |
| | 662 | |
|
| 1 | 663 | | Write-Host "Enabling ImportDotEnv integration for 'Set-Location', 'cd', and 'sl' commands..." -ForegroundColor Yellow |
| 1 | 664 | | $wrapperFunctionFullName = "$($currentModuleForEnable.Name)\Invoke-ImportDotEnvSetLocationWrapper" |
| 1 | 665 | | $existingSetLocation = Get-Command Set-Location -ErrorAction SilentlyContinue |
| 1 | 666 | | if ($existingSetLocation -and $existingSetLocation.CommandType -eq [System.Management.Automation.CommandTypes]::Alias) |
| 0 | 667 | | if (Get-Alias -Name Set-Location -ErrorAction SilentlyContinue) { |
| 0 | 668 | | Remove-Item -Path Alias:\Set-Location -Force -ErrorAction SilentlyContinue |
| | 669 | | } |
| | 670 | | } |
| 1 | 671 | | Set-Alias -Name Set-Location -Value $wrapperFunctionFullName -Scope Global -Force -Option ReadOnly,AllScope |
| 1 | 672 | | Import-DotEnv -Path $PWD.Path |
| 1 | 673 | | Write-Host "ImportDotEnv 'Set-Location', 'cd', 'sl' integration enabled!" -ForegroundColor Green |
| | 674 | | } |
| | 675 | |
|
| | 676 | | function Disable-ImportDotEnvCdIntegration { |
| | 677 | | [CmdletBinding()] |
| | 678 | | param() |
| 1 | 679 | | Write-Host "Disabling ImportDotEnv integration for 'Set-Location', 'cd', and 'sl'..." -ForegroundColor Yellow |
| 1 | 680 | | $currentModuleName = $MyInvocation.MyCommand.Module.Name |
| 1 | 681 | | if (-not $currentModuleName) { |
| 0 | 682 | | Write-Warning "Disable-ImportDotEnvCdIntegration: Could not determine module name. Assuming 'ImportDotEnv'." |
| 0 | 683 | | $currentModuleName = "ImportDotEnv" |
| | 684 | | } |
| 1 | 685 | | $wrapperFunctionFullName = "$currentModuleName\Invoke-ImportDotEnvSetLocationWrapper" |
| 1 | 686 | | $proxiesRemoved = $false |
| | 687 | |
|
| 1 | 688 | | $slCmdInfo = Get-Command "Set-Location" -ErrorAction SilentlyContinue |
| 1 | 689 | | if ($slCmdInfo -and $slCmdInfo.CommandType -eq 'Alias' -and $slCmdInfo.Definition -eq $wrapperFunctionFullName) { |
| 1 | 690 | | Remove-Item -Path Alias:\Set-Location -Force -ErrorAction SilentlyContinue |
| 1 | 691 | | $proxiesRemoved = $true |
| | 692 | | } |
| | 693 | |
|
| 1 | 694 | | Remove-Item -Path Alias:\Set-Location -Force -ErrorAction SilentlyContinue |
| 1 | 695 | | Remove-Item "Function:\Global:Set-Location" -Force -ErrorAction SilentlyContinue |
| | 696 | |
|
| 1 | 697 | | $finalSetLocation = Get-Command "Set-Location" -ErrorAction SilentlyContinue |
| 1 | 698 | | if ($null -eq $finalSetLocation -or $finalSetLocation.Source -ne "Microsoft.PowerShell.Management" -or $finalSetLocati |
| 0 | 699 | | Write-Warning "Disable-ImportDotEnvCdIntegration: 'Set-Location' may not be correctly restored to the original cmdle |
| | 700 | | } |
| | 701 | |
|
| 1 | 702 | | if ($proxiesRemoved) { |
| 1 | 703 | | Write-Host "ImportDotEnv 'Set-Location' integration disabled, default command behavior restored." -ForegroundColor M |
| | 704 | | } else { |
| 1 | 705 | | Write-Host "ImportDotEnv 'Set-Location' integration was not active or already disabled." -ForegroundColor Magenta |
| | 706 | | } |
| 1 | 707 | | Write-Host "Active .env variables (if any) remain loaded. Use 'Import-DotEnv -Unload' to unload them." -ForegroundColo |
| | 708 | | } |
| | 709 | |
|
| 1 | 710 | | Export-ModuleMember -Function Import-DotEnv, |
| | 711 | | Enable-ImportDotEnvCdIntegration, |
| | 712 | | Disable-ImportDotEnvCdIntegration, |
| | 713 | | Invoke-ImportDotEnvSetLocationWrapper |
| | 714 | |
|
| | 715 | | function Restore-EnvVars { |
| | 716 | | param( |
| | 717 | | [hashtable]$VarsToRestoreByFileMap = $null, |
| | 718 | | [string[]]$VarNames = $null, |
| | 719 | | [hashtable]$TrueOriginalEnvironmentVariables, |
| | 720 | | [string]$BasePath = $PWD.Path |
| | 721 | | ) |
| 1 | 722 | | $restorationActions = @() |
| 1 | 723 | | if ($VarsToRestoreByFileMap) { |
| 1 | 724 | | foreach ($fileKey in $VarsToRestoreByFileMap.Keys) { |
| 1 | 725 | | foreach ($var in $VarsToRestoreByFileMap[$fileKey]) { |
| 1 | 726 | | $restorationActions += [PSCustomObject]@{ VarName = $var; SourceFile = $fileKey } |
| | 727 | | } |
| | 728 | | } |
| | 729 | | } |
| 1 | 730 | | if ($VarNames) { |
| 0 | 731 | | $restorationActions += $VarNames | ForEach-Object { [PSCustomObject]@{ VarName = $_; SourceFile = $null } } |
| | 732 | | } |
| | 733 | |
|
| | 734 | | function Restore-EnvVar { |
| | 735 | | param( |
| | 736 | | [string]$VarName, |
| | 737 | | [hashtable]$TrueOriginalEnvironmentVariables, |
| | 738 | | [string]$SourceFile = $null |
| | 739 | | ) |
| | 740 | | function Set-OrUnset-EnvVar { |
| | 741 | | param( |
| | 742 | | [string]$Name, |
| | 743 | | [object]$Value |
| | 744 | | ) |
| 1 | 745 | | if ($null -eq $Value) { |
| 1 | 746 | | [Environment]::SetEnvironmentVariable($Name, $null, 'Process') |
| 1 | 747 | | Remove-Item "Env:\$Name" -Force -ErrorAction SilentlyContinue |
| | 748 | | } else { |
| 1 | 749 | | [Environment]::SetEnvironmentVariable($Name, $Value) |
| | 750 | | } |
| | 751 | | } |
| | 752 | |
|
| 1 | 753 | | $originalValue = $TrueOriginalEnvironmentVariables[$VarName] |
| 1 | 754 | | Set-OrUnset-EnvVar -Name $VarName -Value $originalValue |
| 1 | 755 | | $restoredActionText = if ($null -eq $originalValue) { "Unset" } else { "Restored" } |
| 1 | 756 | | $hyperlink = if ($SourceFile) { |
| 1 | 757 | | Format-VarHyperlink -VarName $VarName -FilePath $SourceFile -LineNumber 1 |
| | 758 | | } else { |
| 0 | 759 | | $searchUrl = "vscode://search/search?query=$([System.Uri]::EscapeDataString($VarName))" |
| 0 | 760 | | "$script:e]8;;$searchUrl$script:e\$VarName$script:e]8;;$script:e\" |
| | 761 | | } |
| 1 | 762 | | Write-Host " $script:itemiser $restoredActionText environment variable: " -NoNewline |
| | 763 | |
|
| | 764 | | # Write-Host ($hyperlink -ne $null ? $hyperlink : $VarName) -ForegroundColor Yellow |
| 1 | 765 | | $Output = if ($null -ne $hyperlink) { $hyperlink } else { $VarName } |
| 1 | 766 | | Write-Host $Output -ForegroundColor Yellow |
| | 767 | | } |
| | 768 | |
|
| 1 | 769 | | $restorationActions | Group-Object SourceFile | ForEach-Object { |
| 1 | 770 | | $fileKey = $_.Name |
| | 771 | |
|
| | 772 | | # Write-Host ($fileKey ? "$script:itemiserA Restoring .env file $(Format-EnvFilePath -Path $fileKey -BasePath $BaseP |
| 1 | 773 | | $envMessage = if ($fileKey) { |
| 1 | 774 | | "$script:itemiserA Restoring .env file $(Format-EnvFilePath -Path $fileKey -BasePath $BasePath):" |
| | 775 | | } else { |
| 0 | 776 | | "Restoring environment variables not associated with any .env file:" |
| | 777 | | } |
| 1 | 778 | | Write-Host $envMessage -ForegroundColor Yellow |
| | 779 | |
|
| 1 | 780 | | $_.Group | ForEach-Object { Restore-EnvVar -VarName $_.VarName -TrueOriginalEnvironmentVariables $TrueOriginalEnviro |
| | 781 | | } |
| | 782 | | } |