Created
September 30, 2026 10:17
-
-
Save jborean93/64e07bfb0e6238f1c4289dd138b615bf to your computer and use it in GitHub Desktop.
Proof of concept for resetting a profile after editing. Profile changes only apply to the caller so cannot be within another function
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| New-Module -Name ProfileUtil -ScriptBlock { | |
| # This must live in a module. A module function runs in the module's | |
| # session state, leaving the caller's session state (and its current | |
| # scope) untouched, so the profile can be dot-sourced back into it. | |
| # As a plain function it would share the caller's session state and | |
| # the profile would load into the function's own scope instead. | |
| function Edit-Profile { | |
| [CmdletBinding()] | |
| param() | |
| $ErrorActionPreference = "Stop" | |
| $tmpProfile = $null | |
| try { | |
| $randomId = [Guid]::NewGuid().ToString('N') | |
| $profileDir = Split-Path -Parent $profile | |
| $fileName = Split-Path -Leaf $profile | |
| $tmpProfile = Join-Path $profileDir ".tmp.$randomId.$fileName" | |
| if (Test-Path -LiteralPath $profile) { | |
| Copy-Item -LiteralPath $profile -Destination $tmpProfile -Force | |
| } | |
| else { | |
| New-Item -ItemType File -Path $tmpProfile -Force | Out-Null | |
| } | |
| # Open the profile in the appropriate editor based on the platform | |
| # This should probably be improved to handle the various ways an | |
| # editor could be set or how it is run. | |
| if (-not $IsWindows) { | |
| & $env:EDITOR $tmpProfile | |
| } | |
| else { | |
| Start-Process -FilePath $tmpProfile -Verb Open -Wait | |
| } | |
| # We parse the temp profile to check for any syntax errors before | |
| # replacing the original profile. | |
| $err = @() | |
| $null = [System.Management.Automation.Language.Parser]::ParseFile( | |
| $tmpProfile, | |
| [ref]$null, | |
| [ref]$err) | |
| if ($err) { | |
| $pe = [System.Management.Automation.ParseException]::new($err) | |
| $eRecord = [System.Management.Automation.ErrorRecord]::new( | |
| $pe, | |
| "ScriptParseException", | |
| [System.Management.Automation.ErrorCategory]::ParserError, | |
| $null) | |
| $PSCmdlet.ThrowTerminatingError($eRecord) | |
| } | |
| # Once validated, replace the original profile with the temp profile | |
| Move-Item -LiteralPath $tmpProfile -Destination $profile -Force | |
| $tmpProfile = $null | |
| # Re-parse from the final location so the scriptblock's file info | |
| # ($PSScriptRoot, $PSCommandPath, error positions) points at | |
| # $profile, not the deleted temp file now that it is validated. | |
| $ast = [System.Management.Automation.Language.Parser]::ParseFile( | |
| $profile, | |
| [ref]$null, | |
| [ref]$null) | |
| # Get an unbound ScriptBlock from the Ast. | |
| $unboundSbk = $ast.GetScriptBlock() | |
| # PSModuleInfo is the only public way to bind a ScriptBlock to an | |
| # arbitrary session state. Create an empty one ($false) and point | |
| # it at the caller's SessionState. | |
| $shim = [psmoduleinfo]::new($false) | |
| $shim.SessionState = $PSCmdlet.SessionState | |
| # Bind the unbound ScriptBlock to the shim so it runs in the | |
| # context of the parent's session state | |
| $bound = $shim.NewBoundScriptBlock($unboundSbk) | |
| # Dot-source so it runs in the caller's *current* scope. Using & | |
| # would create a child scope and discard everything the profile | |
| # defines. | |
| $null = . $bound | |
| } | |
| finally { | |
| if ($tmpProfile) { | |
| Remove-Item -LiteralPath $tmpProfile -Force | |
| } | |
| } | |
| } | |
| } | Import-Module |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment