Skip to content

Instantly share code, notes, and snippets.

@jborean93
Created September 30, 2026 10:17
Show Gist options
  • Select an option

  • Save jborean93/64e07bfb0e6238f1c4289dd138b615bf to your computer and use it in GitHub Desktop.

Select an option

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
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