You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Users who source ocx shell env or ocx shell profile load multiple times per session (.bashrc/.zshrc, CI re-runs, nested shells) get a growing PATH with duplicate entries. This slows command lookup, makes echo $PATH unreadable, and causes ordering bugs. With this fix, generated shell output is idempotent — safe to eval repeatedly, readable when printed, and no shell pollution.
Context
Shell::export_path() in crates/ocx_lib/src/shell.rs:120 generates simple prepend commands like export PATH="/new:$PATH". No deduplication. Three surfaces affected:
ProfileBuilder::add() — delegates to export_path()
Env::add_path() — runtime env for ocx exec
Industry standard: fish has fish_add_path --move, zsh has typeset -U path, mise/direnv/nix all emit idempotent snippets with helper functions. A tool that pollutes PATH on repeated sourcing is a bug.
Scope
Update Shell::export_path() to generate dedup+prepend for all 9 shells
Update Env::add_path() to deduplicate at runtime
Helper function pattern for shells that need it (defined once, unset after — no pollution)
Native syntax for shells that support clean one-liners
Batch: simple prepend only (documented limitation)
Out of scope: constant variable handling (already has conflict detection)
Design
Two strategies based on shell capabilities:
Strategy A — Native syntax (no helper needed): Zsh, Fish, and Elvish have built-in array/list operations that express dedup+prepend cleanly in a single line.
Strategy B — Helper function (defined once, unset after): Bash, POSIX shells, and PowerShell need multi-step logic. A helper function is defined once at the top of the output, called N times for each PATH-type var, then unset at the end. This follows the pattern used by mise (_mise_hook), direnv (_direnv_hook), and nix (_nix_import_env). Users who need structured copy-paste output already have ocx env (key-value table / JSON).
Shell
Strategy
Mechanism
External deps
Zsh
Native
path=(/val "${(@)path:#/val}")
None
Fish
Native
fish_add_path --prepend --move --path /val
None (builtin, fish 3.2+)
Elvish
Native
set paths = [/val (each {|p| if (!=s $p /val) { put $p }} $paths)]
None
Bash
Helper
__ocx_prepend() using read -ra array split + loop
None
Dash/Ash/Ksh
Helper
__ocx_prepend() using awk -v RS=:
awk (POSIX.1 required)
PowerShell
Helper
__ocx_prepend() using -split + Where-Object
None
Batch
Prepend only
SET "PATH=/val;%PATH%"
N/A
Example generated output for bash with two packages:
flowchart TD
A["Package metadata: PATH type var"] --> B{"Output target?"}
B -->|"shell env / profile load"| C["Shell::export_path()"]
B -->|"exec / env"| D["Env::add_path()"]
C --> E{"Shell capabilities?"}
E -->|"Zsh / Fish / Elvish"| F["Native dedup syntax\nOne clean line per var"]
E -->|"Bash / Dash / Ash / Ksh / PowerShell"| G["Helper function\nDefined once → called N times → unset"]
E -->|"Batch"| H["Simple prepend\nDedup not feasible"]
D --> I["Split on separator → filter exact matches → prepend → rejoin"]
style F fill:#d4edda
style G fill:#fff3cd
style H fill:#f8d7da
Loading
Why helper functions over self-contained lines
Primary consumers are eval "$(ocx shell env ...)" — programmatic eval, not copy-paste
ocx env already serves the structured copy-paste use case (key-value / JSON)
A 150-char bash one-liner per PATH entry is unreadable when printed to terminal
mise, direnv, nix all use helper functions in eval output — established pattern
Helper is unset after use — zero shell pollution
Why awk is safe to depend on for POSIX shells
awk is POSIX.1 required — present on Alpine (ash), Debian minimal (dash), BusyBox
Docker scratch images lack awk but also lack a shell to source the output
The alternative (while IFS=: read loop) is fragile with edge cases around IFS restoration
Why no dedup for batch
CMD.exe has no array type, no string split, no filter primitive
FOR /F dedup requires multi-line syntax + SETLOCAL ENABLEDELAYEDEXPANSION
Not expressible as a function pattern (CMD has no function scope)
Batch scripts are rarely sourced repeatedly (no .bashrc equivalent)
Value
Users who source
ocx shell envorocx shell profile loadmultiple times per session (.bashrc/.zshrc, CI re-runs, nested shells) get a growing PATH with duplicate entries. This slows command lookup, makesecho $PATHunreadable, and causes ordering bugs. With this fix, generated shell output is idempotent — safe to eval repeatedly, readable when printed, and no shell pollution.Context
Shell::export_path()incrates/ocx_lib/src/shell.rs:120generates simple prepend commands likeexport PATH="/new:$PATH". No deduplication. Three surfaces affected:Shell::export_path()— shell export statements forshell env,shell profile loadProfileBuilder::add()— delegates toexport_path()Env::add_path()— runtime env forocx execIndustry standard: fish has
fish_add_path --move, zsh hastypeset -U path, mise/direnv/nix all emit idempotent snippets with helper functions. A tool that pollutes PATH on repeated sourcing is a bug.Scope
Shell::export_path()to generate dedup+prepend for all 9 shellsEnv::add_path()to deduplicate at runtimeDesign
Two strategies based on shell capabilities:
Strategy A — Native syntax (no helper needed): Zsh, Fish, and Elvish have built-in array/list operations that express dedup+prepend cleanly in a single line.
Strategy B — Helper function (defined once, unset after): Bash, POSIX shells, and PowerShell need multi-step logic. A helper function is defined once at the top of the output, called N times for each PATH-type var, then
unsetat the end. This follows the pattern used by mise (_mise_hook), direnv (_direnv_hook), and nix (_nix_import_env). Users who need structured copy-paste output already haveocx env(key-value table / JSON).path=(/val "${(@)path:#/val}")fish_add_path --prepend --move --path /valset paths = [/val (each {|p| if (!=s $p /val) { put $p }} $paths)]__ocx_prepend()usingread -raarray split + loop__ocx_prepend()usingawk -v RS=:awk(POSIX.1 required)__ocx_prepend()using-split+Where-ObjectSET "PATH=/val;%PATH%"Example generated output for bash with two packages:
Example generated output for zsh (no helper):
flowchart TD A["Package metadata: PATH type var"] --> B{"Output target?"} B -->|"shell env / profile load"| C["Shell::export_path()"] B -->|"exec / env"| D["Env::add_path()"] C --> E{"Shell capabilities?"} E -->|"Zsh / Fish / Elvish"| F["Native dedup syntax\nOne clean line per var"] E -->|"Bash / Dash / Ash / Ksh / PowerShell"| G["Helper function\nDefined once → called N times → unset"] E -->|"Batch"| H["Simple prepend\nDedup not feasible"] D --> I["Split on separator → filter exact matches → prepend → rejoin"] style F fill:#d4edda style G fill:#fff3cd style H fill:#f8d7daWhy helper functions over self-contained lines
eval "$(ocx shell env ...)"— programmatic eval, not copy-pasteocx envalready serves the structured copy-paste use case (key-value / JSON)unsetafter use — zero shell pollutionWhy
awkis safe to depend on for POSIX shellsawkis POSIX.1 required — present on Alpine (ash), Debian minimal (dash), BusyBoxscratchimages lackawkbut also lack a shell to source the outputwhile IFS=: readloop) is fragile with edge cases around IFS restorationWhy no dedup for batch
FOR /Fdedup requires multi-line syntax +SETLOCAL ENABLEDELAYEDEXPANSION.bashrcequivalent)Acceptance Criteria
ocx shell env pkgtwice produces no duplicate PATH entries (all shells except batch)ocx shell profile loadtwice produces no duplicate PATH entriesocx execruntime env has no duplicate PATH entriesunset/removed after use — no shell namespace pollution/usr/binnot affected when adding/usr/bin/extra)awkinvocation per PATH entryDependencies
None
References
fish_add_pathbuiltin (fish 3.2+, 2021)typeset -U path/${(@)path:#pattern}array filteringawk -v RS=:dedup pattern (Linux Journal)