<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>~iany/ Environment Variables</title><link>https://blog.iany.me/tags/environment-variables/</link><description>Recent content in Environment Variables «~iany/»</description><language>en-US</language><managingEditor>me@iany.me (Ian Yang)</managingEditor><webMaster>me@iany.me (Ian Yang)</webMaster><copyright>CC-BY-SA 4.0</copyright><lastBuildDate>Sat, 06 Apr 2024 08:55:34 +0800</lastBuildDate><atom:link href="https://blog.iany.me/tags/environment-variables/index.xml" rel="self" type="application/rss+xml"/><item><title>Envrc Alternative for PowerShell in Windows</title><link>https://blog.iany.me/2024/04/envrc-alternative-for-powershell-in-windows/</link><pubDate>Sat, 06 Apr 2024 08:55:34 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2024/04/envrc-alternative-for-powershell-in-windows/</guid><description>&lt;p&gt;This post introduces a solution for automatically setting up and tearing down shell environments for PowerShell in Windows. It is proposed as a potential alternative to the bash-based tool &lt;a href="https://github.com/direnv/direnv"&gt;direnv&lt;/a&gt;, which, while effective at loading &lt;code&gt;.envrc&lt;/code&gt; files in the current or nearest ancestor directory, has limited compatibility with PowerShell in Windows.&lt;/p&gt;
&lt;h2 id="envrcps1"&gt;.envrc.ps1&lt;/h2&gt;
&lt;p&gt;The basic idea is creating a file &lt;code&gt;.envrc.ps1&lt;/code&gt; with two entries: setup and teardown. The setup entry is responsible for setting up the environment up entering the directory, and the teardown entry is used when leaving it.&lt;/p&gt;
&lt;p&gt;PowerShell allows code to be executed from a file using the dot command. This means that the entire file can be used as the setup entry. For the teardown entry, a function can be defined within the same file, and I use the function name &lt;code&gt;down&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The layout of the file &lt;code&gt;.envrc.ps1&lt;/code&gt; looks like:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-powershell"&gt;# setup
$env:GH_TOKEN = &amp;quot;secret&amp;quot;
# teardown
function global:down {
$env:GH_TOKEN = &amp;quot;&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;global:&lt;/code&gt; scope before the function name is necessary when this file is loaded in the hook, as I will do later.&lt;/p&gt;
&lt;p&gt;The corresponding commands for setup and teardown entries:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Setup: &lt;code&gt;. .envrc.ps1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Teardown: &lt;code&gt;down&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="hook"&gt;Hook&lt;/h2&gt;
&lt;p&gt;To hook the setup and teardown entries, I reference &lt;a href="https://github.com/PowerShell/PowerShell/issues/14484#issuecomment-1731647083"&gt;this example&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-powershell"&gt;using namespace System;
using namespace System.Management.Automation;
$hook = [EventHandler[LocationChangedEventArgs]] {
param([object] $source, [LocationChangedEventArgs] $eventArgs)
end {
# 1. `down` for $eventArgs.OldPath
# 2. `. .envrc.ps1` for $eventArgs.NewPath
}
};
$currentAction = $ExecutionContext.SessionState.InvokeCommand.LocationChangedAction;
if ($currentAction) {
$ExecutionContext.SessionState.InvokeCommand.LocationChangedAction = [Delegate]::Combine($currentAction, $hook);
} else {
$ExecutionContext.SessionState.InvokeCommand.LocationChangedAction = $hook;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To run the code snippet, PowerShell 7 or later is required. You can install it from the &lt;a href="https://github.com/PowerShell/PowerShell"&gt;repository&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Here is the algorithm for identifying setup and teardown entries:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Find the nearest &lt;code&gt;.envrc.ps1&lt;/code&gt; for the old path.&lt;/li&gt;
&lt;li&gt;Find the nearest &lt;code&gt;.envrc.ps1&lt;/code&gt; for the new path.&lt;/li&gt;
&lt;li&gt;If the two paths are the same, skip the next step.&lt;/li&gt;
&lt;li&gt;If they differ, invoke the &lt;code&gt;down&lt;/code&gt; function and remove it if it exists. Then, if found, load the &lt;code&gt;.envrc.ps1&lt;/code&gt; file for the new path.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The full code example to be added into PowerShell &lt;code&gt;$PROFILE&lt;/code&gt; file.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-powershell"&gt;using namespace System;
using namespace System.Management.Automation;
# existing PowerShell profile contents
# skip when version is less than 7
if ($PSVersionTable.PSVersion.Major -lt 7) {
return
}
# find the .envrc.ps1 in the currenct directory or the nearest ancestor directory.
function Find-NearestEnvrc {
param (
[Parameter(Mandatory=$true)]
[string]$StartDir
)
$currentDir = Resolve-Path $StartDir
while ($currentDir -ne &amp;quot;&amp;quot;) {
$envrcPath = Join-Path $currentDir &amp;quot;.envrc.ps1&amp;quot;
if (Test-Path $envrcPath) {
return $envrcPath
}
$currentDir = Split-Path $currentDir -Parent
}
return &amp;quot;&amp;quot;
}
# hook the setup and teardown entries
$hook = [EventHandler[LocationChangedEventArgs]] {
param([object] $source, [LocationChangedEventArgs] $eventArgs)
end {
$oldEnvrc = Find-NearestEnvrc $eventArgs.OldPath
$newEnvrc = Find-NearestEnvrc $eventArgs.NewPath
if ($oldEnvrc -ne $newEnvrc) {
Get-Command down -ErrorAction SilentlyContinu
if (Get-Command down -ErrorAction SilentlyContinu) {
down
Remove-Item Function:down
}
if ($newEnvrc -ne &amp;quot;&amp;quot;) {
. $newEnvrc
}
}
}
};
$currentAction = $ExecutionContext.SessionState.InvokeCommand.LocationChangedAction;
if ($currentAction) {
$ExecutionContext.SessionState.InvokeCommand.LocationChangedAction = [Delegate]::Combine($currentAction, $hook);
} else {
$ExecutionContext.SessionState.InvokeCommand.LocationChangedAction = $hook;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="example-usage"&gt;Example Usage&lt;/h2&gt;
&lt;pre&gt;&lt;code class="language-powershell"&gt;# setup
# set environment variable
$env:GH_TOKEN = &amp;quot;secret&amp;quot;
# load python virtual env
. .venv\Scripts\Activate.ps1
# add a helper function
function global:build {
cargo build
}
# teardown
function global:down {
# unset environment variable
$env:GH_TOKEN = &amp;quot;&amp;quot;
# unload python virtual env
deactivate
# remove the helper function
Remove-Item Function:build
}
&lt;/code&gt;&lt;/pre&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/environment-variables/">Environment Variables</category><category domain="https://blog.iany.me/tags/powershell/">PowerShell</category><category domain="https://blog.iany.me/tags/windows/">Windows</category></item></channel></rss>