Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 

Repository files navigation

projsSelector: Symlink-Driven Project Environment Switcher

The Idea and Motivation

When working across multiple projects, each project typically needs:

  • Its own directory prepended to PATH (for project-specific executables)
  • Its own Python virtual environment activated (for isolated dependencies)
  • A clean way to undo all of the above when switching away

The naive approach is a separate enable/disable script per project. That means duplicating the same logic across every project, and every improvement must be applied N times.

projsSelector solves this with a single script (projsSelector.shDot) and a set of symlinks. The symlink’s filename is the contract: it encodes both the action (projEnable or projDisable) and the project name (this, other, …) separated by _. When sourced, the script reads BASH_SOURCE[0] to extract both parts and branches on each independently. Adding a new project is a filesystem operation — create two symlinks and add one elif line.

Structure at a Glance

The figure below shows the full flow from shell login to project environment effects — four layers read top to bottom. The colours carry meaning; scan the legend first, then the figure.

Legend:

ElementMeaning
Cyan fillShell bootstrap: ~/.bashrc, projsEnabler.shDot, aliases.
Yellow fillSymlink. Points to projsSelector.shDot; filename is the contract.
Green fillThe real script. Reads BASH_SOURCE[0] to extract action and project name.
Blue fillDispatch decision. Two independent axes: action and project.
Pink fillEffect / outcome produced in the current shell session.
Solid edgeDirect source or invocation.
Dashed edgeSymlink resolution or alias definition.
Dotted edgeAlias invocation (triggers the source chain).

images/projsSelector.png

Reading the figure, top to bottom:

  • Bootstrap layer. At login, ~/.bashrc sources projsEnabler.shDot, which defines two aliases: projEnable_this and projEnable_other. These are the only entry points a user ever types. Note that no projDisable_xxx alias is registered here — it is created dynamically by the script at enable time.
  • Symlink layer. All four proj{Enable,Disable}_{this,other}.sh files are symlinks to the single real script projsSelector.shDot. Invoking a projEnable_xxx alias sources the corresponding symlink, which is how the chain reaches the script.
  • Dispatch layer. The script reads BASH_SOURCE[0]##*/ to get the symlink’s own filename and branches on two independent axes: the action part (projEnable vs projDisable) determines what to do, and the project part (this, other, …) determines which resources to use.
  • Effects layer. On enable: PATH is prepended, the project venv is activated, and a projDisable_xxx alias is registered for later use. On disable: the venv is deactivated and PATH is restored.

File Layout

projsSelector.shDot        ← the single real script (source of truth)
projEnable_this.sh         → projsSelector.shDot
projDisable_this.sh        → projsSelector.shDot
projEnable_other.sh        → projsSelector.shDot
projDisable_other.sh       → projsSelector.shDot
projsEnabler.shDot         ← sourced from ~/.bashrc to register aliases at login
pyVenv-this-sbom.pcs       ← provisions the Python venv for project "this"
pyVenv-other-sbom.pcs      ← provisions the Python venv for project "other"

Bootstrapping: ~/.bashrc Hook

projsEnabler.shDot registers a shell alias for each project’s enable script. Without it, you would have to type the full path to source projEnable_this.sh each time. Add the following to ~~/.bashrc~:

if [ -f /de/example/bin/projsEnabler.shDot ] ; then
   source /de/example/bin/projsEnabler.shDot
fi

projsEnabler.shDot contains:

de_example_bin="/de/example/bin"

alias projEnable_this=". ${de_example_bin}/projEnable_this.sh"
alias projEnable_other=". ${de_example_bin}/projEnable_other.sh"

Note that only projEnable_xxx aliases are registered here. projDisable_xxx is defined dynamically as a shell alias when the corresponding projEnable_xxx runs — it is available for the lifetime of that shell session.

Usage

Enabling a project

projEnable_this

This sources projEnable_this.sh (a symlink to projsSelector.shDot), which:

  1. Prepends /de/example/bin to PATH
  2. Activates the Python venv at /de/example/venv/this
  3. Registers a projDisable_this alias in the current shell

Disabling a project

projDisable_this

This sources projDisable_this.sh (also a symlink to projsSelector.shDot), which:

  1. Calls venv deactivate (restoring the PATH snapshot taken at activate time)
  2. Removes /de/example/bin from PATH

Switching between projects

Simply enable the new project — if a venv is already active, projEnable_xxx detects it, deactivates it first, then activates the new one. You do not need to manually disable before enabling another.

Mechanics: How the Symlink Dispatch Works

The filename of the symlink being sourced is the contract. It encodes two parts separated by _:

projEnable_this.sh
^^^^^^^^^^ ^^^^
action     project-name

At source time, BASH_SOURCE[0] gives the symlink’s own name (not the master file’s name), so both parts are available for branching:

  • The action part (projEnable vs projDisable) drives what to do.
  • The project part (this, other, …) drives which resources to use.

This is a well-established Unix pattern — the same mechanism used by busybox, where one binary behaves differently depending on the name it was invoked under.

Adding a New Project

To add a project named xxx:

  1. Create the symlinks:
ln -s projsSelector.shDot projEnable_xxx.sh
ln -s projsSelector.shDot projDisable_xxx.sh
  1. Add an elif branch in the project-name detection block in projsSelector.shDot:
elif [[ "${FileBeingSourced}" == *_xxx.* ]]; then
    proj_venvBinPath="/de/example/venv/xxx/bin"
  1. Add the alias to projsEnabler.shDot:
alias projEnable_xxx=". ${de_example_bin}/projEnable_xxx.sh"
  1. Provision the venv (copy and adapt pyVenv-this-sbom.pcspyVenv-xxx-sbom.pcs).

No changes to the dispatch logic are needed.

Prior Art and Related Work

Python venv activate / deactivate

Every Python virtual environment created with python3 -m venv ships an activate script and a deactivate function. projsSelector does not replace these — it wraps them. activate / deactivate deliberately scope themselves to the venv’s own bin/ directory and know nothing about a project’s separate executables directory or about switching between named projects. projsSelector adds the layer that activate / deactivate omit:

  • manages the project bin/ path (/de/example/bin) separately from the venv path
  • provides a named-project entry point (projEnable_this) callable from anywhere
  • guards against non-nestable activate by deactivating any active venv first
  • wires projDisable_xxx so the full undo is one command

activate and deactivate are used as primitives inside projsSelector — they compose, not compete.

direnv

direnv automatically sources a .envrc file when you cd into a directory and unloads it when you leave. It is the most widely used tool in this space. The key difference in philosophy: direnv is location-triggered (entering a directory), while projsSelector is explicit (you invoke projEnable_xxx by name). direnv also requires a shell hook installed in .bashrc and a running helper process. projsSelector is pure bash with no daemon.

virtualenvwrapper

virtualenvwrapper provides workon <project> style switching between named Python environments. It is Python-specific and opinionated about where venvs live ($WORKON_HOME). projsSelector is not Python-specific — the venv integration is one feature, and the proj_binPath management is independent of it.

busybox / multicall binaries

The symlink dispatch pattern in projsSelector is the sourced-script analogue of the busybox multicall binary: one binary (or one script) behaves differently depending on the name it was invoked under. busybox uses argv[0]; projsSelector uses BASH_SOURCE[0]. This pattern is well-established for executables but rarely seen applied to sourced shell scripts.

update-alternatives

update-alternatives (Debian/Red Hat) manages symlink farms at the OS level to select among competing implementations of a command. The filesystem idea is the same — symlinks as dispatch — but the scope is system-wide executable selection, not per-user project environment switching.

Dependencies

  • path_contains, path_prepend, path_remove — provided by the lpDo/lp shell library.
  • activate / deactivate — standard Python venv scripts (created by python3 -m venv).

About

Bash sourced machinery for selecting (activate/deactivate) projects in a unified manner

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages