A lightweight, cross-platform PATH management tool for dotfiles that work across multiple operating systems (macOS and Linux for now) and shells.
Pathuni reads a simple YAML config file and generates shell-specific PATH export commands. It validates that directories exist before including them and supports platform-specific path lists.
# Install latest release
curl -sSL https://raw.githubusercontent.com/pmdci/pathuni/main/install.sh | bashbrew tap pmdci/pathuni
brew install pathunigit clone https://github.com/pmdci/pathuni
cd pathuni
make build
make install # copies to ~/.local/binDownload pre-built binaries from the releases page.
macOS Users: Downloaded binaries may be blocked by Gatekeeper. After downloading, run:
xattr -d com.apple.quarantine pathuniOr alternatively:
codesign -s - pathuni # You might need additional flagsCreate ~/.config/pathuni/my_paths.yaml:
all:
tags: [base, essential] # Platform-level tags
paths:
- "$HOME/.local/bin" # Inherits: [base, essential]
- path: "$HOME/.cargo/bin" # Explicit tags override inheritance
tags: [rust, dev]
macos:
tags: [mac, gui] # Platform-level tags for macOS
paths:
- "/opt/homebrew/bin" # Inherits: [mac, gui]
- path: "/opt/homebrew/sbin" # Explicit tags override
tags: [admin, homebrew]
- path: "/Applications/Docker.app/Contents/Resources/bin"
tags: [docker, work]
- path: "/usr/local/special" # Explicit empty override
tags: [] # No tags (breaks inheritance)
linux:
# No tags field - platform has no tags to inherit
paths:
- "/home/linuxbrew/.linuxbrew/bin" # No inheritance = no tags
- "/usr/local/bin" # No inheritance = no tags
- path: "/home/linuxbrew/.linuxbrew/sbin" # Explicit tags still work
tags: [admin, homebrew]
- path: "/opt/simple/bin" # Path object without tags field
# Missing tags field + no platform tags = no tagsPowerShell on macOS doesn't automatically load system paths from /etc/paths and /etc/paths.d/ like Unix shells do. You can enable this with:
macos:
powershell:
include_system_paths: true # Loads system paths for PowerShellWith this setting, PowerShell will get the same comprehensive PATH that zsh/bash get automatically, including standard system directories like /usr/bin, /bin, etc.
You can control how those macOS system paths are classified and whether they participate in tag filtering:
macos:
tags: [mac]
powershell:
include_system_paths: true
include_system_paths_as: system # or pathuni (default: system)
# tags: [sys] # optional; used only when as=pathuni-
include_system_paths_as: system(default):- Treats
/etc/pathsand/etc/paths.d/*entries as System. - Dry-run marks them as
[.]and counts as System. - Prune missing paths with
-p system|all; tag filters do not apply.
- Treats
-
include_system_paths_as: pathuni:- Injects those entries on the Pathuni side.
- Dry-run marks them as
[+]and counts as Pathuni. - Subject to tag filters and
-p pathuni|all. - Tag semantics when
as=pathuni:- tags omitted → inherit platform tags (like simple string paths)
tags: [sys]→ explicit override tagstags: []→ explicit empty tags (break inheritance)
Quick examples:
# A) Default: classify as system
macos:
powershell:
include_system_paths: true
# include_system_paths_as: system (implicit)
# B) Taggable: classify as pathuni and attach tags
macos:
powershell:
include_system_paths: true
include_system_paths_as: pathuni
tags: [sys]
# C) Inherit platform tags
macos:
tags: [mac]
powershell:
include_system_paths: true
include_system_paths_as: pathuni
# tags omitted → inherit [mac]
# D) Break inheritance (no tags)
macos:
tags: [mac]
powershell:
include_system_paths: true
include_system_paths_as: pathuni
tags: []You can now define tags at the platform level (all, macos, linux) that are automatically inherited by simple string paths. This reduces repetition and makes configuration more maintainable:
all:
tags: [base, essential] # All simple paths inherit these tags
paths:
- "/usr/local/bin" # Gets tags: [base, essential]
- "/usr/bin" # Gets tags: [base, essential]
- path: "/special/bin" # Explicit tags override inheritance
tags: [admin, work] # Gets tags: [admin, work] - no inheritance
- path: "/no/tags/bin" # Explicit empty array breaks inheritance
tags: [] # Gets no tags (not [base, essential])
macos:
tags: [mac, desktop] # macOS-specific inheritance
paths:
- "/opt/homebrew/bin" # Gets tags: [mac, desktop]
- path: "/Applications/Docker.app/Contents/Resources/bin"
tags: [docker] # Gets tags: [docker] - overrides inheritanceKey inheritance rules:
- Simple string paths (like
"/usr/local/bin") inherit platform tags - Explicit path objects with
tags:field override inheritance completely - Empty tags array (
tags: []) explicitly means "no tags" (breaks inheritance) - Missing tags field means "inherit platform tags"
This is especially powerful for filtering:
# Include only paths with platform-specific tags
pathuni dry-run --tags-include=mac # Only macOS-tagged paths
pathuni dry-run --tags-include=base # Only base-tagged paths from 'all'
# Exclude specific platforms
pathuni dry-run --tags-exclude=linux # Exclude Linux-tagged pathsYou can filter paths by tags using --tags-include and --tags-exclude flags. Tags support both OR logic (comma-separated) and AND logic (plus-separated):
# Include only paths tagged with 'dev'
pathuni --tags-include=dev
# Include paths tagged with 'dev' OR 'work'
pathuni --tags-include=dev,work
# Include paths that have BOTH 'work' AND 'admin' tags
pathuni --tags-include=work+admin
# Exclude paths tagged with 'docker'
pathuni --tags-exclude=docker
# Exclude paths tagged with 'docker' OR 'gaming'
pathuni --tags-exclude=docker,gaming
# Exclude paths that have BOTH 'work' AND 'admin' tags
pathuni --tags-exclude=work+admin
# Complex: include 'dev' paths but exclude 'work' paths
pathuni --tags-include=dev --tags-exclude=workImportant:
- Untagged paths are immune to tag filtering and are always included
- Exclude wins - if a path matches both include and exclude conditions, it's excluded
- No tag flags - all paths (tagged and untagged) are included
Tag naming rules:
- Exact tags: 3-20 characters, start with a letter, only letters/numbers/underscores
- Examples:
dev,work_laptop,gaming2,MyProject
- Examples:
- Wildcard patterns: Support glob-style patterns using
*,?,[...]syntax- Examples:
work_*,server?,[abc]*,*_temp
- Examples:
You can use glob-style wildcard patterns for flexible tag matching, perfect for hierarchical tag structures:
# Wildcard patterns using *
pathuni --tags-include="work_*" # Matches: work_prod, work_dev, work_staging
pathuni --tags-exclude="*_temp" # Matches: build_temp, cache_temp, any_temp
# Single character wildcards using ?
pathuni --tags-include="dev?" # Matches: dev1, dev2, devA (exactly 4 chars)
pathuni --tags-exclude="?unt" # Matches: hunt, punt, bunt (exactly 4 chars)
# Character classes using [...]
pathuni --tags-include="server[123]" # Matches: server1, server2, server3
pathuni --tags-exclude="[abc]*" # Matches: app, audio, build, cache...
pathuni --tags-include="[a-z]*" # Matches: any tag starting with a-z
pathuni --tags-exclude="[^test]*" # Matches: any tag NOT starting with t,e,s
# Complex combinations
pathuni --tags-include="work_*,server*" --tags-exclude="*_temp"
# Include work_* OR server* patterns, but exclude anything ending in _temp
# Case-insensitive matching
pathuni --tags-exclude="MA?OS" # Matches: macos, MACOS, MacOS, etc.Supported wildcard syntax:
*- matches any sequence of characters (zero or more)?- matches exactly one character[abc]- matches any character in the set (a, b, or c)[a-z]- matches any character in the range (a through z)[^abc]- matches any character NOT in the set (anything except a, b, c)
Pattern examples:
work_*→work_prod,work_dev,work_stagingdev?→dev1,dev2,devA(but notdevelopment)server[12]→server1,server2(but notserver3)*_temp→build_temp,work_temp,cache_temp[a-c]*→app,build,cache(any tag starting with a, b, or c)
Note: All wildcard matching is case-insensitive, so Work_* matches work_prod, WORK_DEV, etc.
Pathuni now supports a global --scope, -s flag used in all commands and an optional --prune, -p flag:
--scope:pathuni | system | fullpathuni: only paths from your YAML config (subject to tags and pruning)system: only entries from the currentPATHfull: a merge of pathuni and system, with pathuni-first precedence
--prune:none | pathuni | system | all(default:pathuni)none: don’t drop missing paths from either sourcepathuni: drop missing YAML paths onlysystem: drop missing systemPATHentries onlyall: drop missing from both sources
Notes:
- Precedence is pathuni-first in merges. Duplicates are removed with first‑wins.
- Markers used in dry-run:
[+]= pathuni,[.]= system. Skipped markers:[-]= filtered by tags,[!]= pathuni not found,[?]= system not found (only when pruning system). init --defer-env, -dprepends pathuni but references the livePATHat evaluation; it’s incompatible with--prune=system|all(system isn’t expanded).
init: scope=full(pathuni-first), prune=pathuni, defer-env=offdry-run: scope=full, prune=pathunidump: scope=full, prune=pathuni- Deduplication: always on; first‑wins;
fullmerges are pathuni-first
none: include missing YAML and system entriespathuni: drop missing YAML (pathuni) entries onlysystem: drop missing system entries onlyall: drop missing entries from both sources- Dry-run markers:
[?]appears only when pruning system;[!]appears only when pruning pathuni init --defer-env: incompatible with--prune=system|all
# Auto-detect shell and OS (default command)
pathuni
pathuni init
# Specify shell explicitly
pathuni init --shell=fish
pathuni --shell=powershell # shortcut: global flags work on root command
# Specify OS explicitly
pathuni init --os=linux
# Choose scope and pruning
pathuni init -s full # default, pathuni-first merge
pathuni init -s full -p none # include missing YAML + system
pathuni init -s pathuni -p all # only existing YAML entries
# Defer environment (prepend pathuni and reference $PATH)
pathuni init -s full -d # bash/zsh/sh
pathuni init -S fish -s full -d # fish
pathuni init -S powershell -s full -d # PowerShellpathuni dry-run
pathuni n # shortcut
# With specific shell
pathuni dry-run --shell=bash
# With flags
pathuni dry-run -s full -p none # pathuni-first; include missing YAML + system
pathuni dry-run -s full -p system # include missing YAML; prune missing system
pathuni dry-run -s full -p all # prune missing from both# Show all current PATH entries
pathuni dump
pathuni d # shortcut
# Dump output scope options
pathuni dump --scope=pathuni # Only YAML-derived (respects tags/prune)
pathuni dump --scope=system # Only current PATH (respects prune when -p system|all)
pathuni dump --scope=full # pathuni-first merge (default), respects prune
# Different output formats
pathuni dump --format=json
pathuni dump --format=yaml --scope=pathuni
pathuni d -f json -s full # using shortcuts and short flagsExample dry-run outputs (scope + prune):
# All paths included (no filtering)
$ pathuni dry-run --os=macos -s full -p none
Evaluating: /Users/you/.config/pathuni/my_paths.yaml
OS : macOS (specified)
Shell : zsh (detected)
Flags : scope=full, prune=none
5 Included Paths:
[+] /Users/you/.local/bin
[+] /Users/you/.cargo/bin
[+] /opt/homebrew/bin
[.] /usr/bin
[.] /bin
0 Skipped paths
5 Paths included in total
├ 3 Pathuni paths
└ 2 System paths
0 Skipped paths
# With tag filtering showing detailed skip reasons
$ pathuni dry-run -s full -p system --tags-include=essential
Evaluating: /Users/you/.config/pathuni/my_paths.yaml
OS : macOS (detected)
Shell : zsh (detected)
Flags : scope=full, prune=system
3 Included Paths:
[+] /Users/you/.local/bin
[.] /usr/bin
[.] /bin
3 Skipped Paths:
[-] /Users/you/.cargo/bin
└rust,dev != essential
[-] /opt/homebrew/sbin
└admin != essential
[?] /some/missing/system (not found)
3 Paths included in total
├ 1 Pathuni path
└ 2 System paths
3 Paths skipped in total
├ 2 Pathuni paths
└ 1 System path
# Complex filtering with inheritance and explicit empty tags,
# specifying zsh as the shell
$ pathuni dry-run -s full -p all --tags-exclude=gui --shell=zsh
Evaluating: /Users/you/.config/pathuni/my_paths.yaml
OS : macOS (detected)
Shell : zsh (specified)
Flags : scope=full, prune=all
3 Included Paths:
[+] /Users/you/.local/bin
[+] /Users/you/.cargo/bin
[.] /usr/bin
2 Skipped Paths:
[-] /opt/homebrew/bin
└mac = gui
[!] /Applications/Docker.app/Contents/Resources/bin (not found)
3 Paths included in total
├ 2 Pathuni paths
└ 1 System path
2 Paths skipped in total
├ 1 Pathuni path
└ 1 System pathExample dump outputs:
$ pathuni dump --scope=pathuni
/Users/you/.local/bin
/opt/homebrew/bin
/opt/homebrew/sbin
$ pathuni dump --format=yaml --scope=pathuni
PATH:
- /Users/you/.local/bin
- /opt/homebrew/bin
- /opt/homebrew/sbin
$ pathuni dump --format=json --scope=full
{"PATH":["/Users/you/.local/bin","/opt/homebrew/bin",...]}
### Evaluate init output
Evaluate the generated command in your shell initialization:
- bash/zsh/sh
- eval "$(pathuni init -s full -d)"
- fish
- eval (pathuni init -S fish -s full -d)
- PowerShell (Unix)
- pathuni init -S powershell -s full -d | Invoke-Expression- POSIX shells (sh, ash, bash, dash, ksh, mksh, yash, zsh) - uses
export PATH= - fish - uses
set -gx PATH - powershell - uses
$env:PATH =- On macOS, can automatically include system paths from
/etc/pathsand/etc/paths.d/using theinclude_system_pathsYAML setting (see above under Shell-specific Configuration).
- On macOS, can automatically include system paths from
Most dotfiles managers are heavyweight solutions for simple PATH management. Pathuni aims to do one thing well: cross-platform PATH exports with validation and flexible tag-based filtering, perfect for developers juggling multiple environments without wanting full dotfiles orchestration.
Key features:
- Cross-platform: Works on macOS, Linux with plans for Windows/*BSD
- Multi-shell: bash, zsh, fish, PowerShell support
- Platform-level tag inheritance: Define tags once per platform, inherit automatically
- Tag-based filtering: Include/exclude paths by context (dev, work, gaming, etc.)
- Wildcard tag patterns: Use glob-style patterns (
work_*,server?,[abc]*) for flexible filtering - Improved dry-run output: Tree-structured output with detailed skip reasons
- Path validation: Only includes directories that actually exist
- Lightweight: Single binary, no dependencies
- Mixed format: Support both simple strings and tagged path entries
Pull requests, bug reports, and feature suggestions are welcome!
Areas that could use help:
- Windows support
- *BSD support
- Additional shell support:
- C shells (csh, tcsh, ...)
- Next-gen, post-POSIX shells (elvish, nushell (nu), xonsh, ...)
- Performance improvements
make build # Build optimised binary to bin/pathuni
make build-release # Build with maximum optimisation + UPX compression (if available)
make cross-compile # Build for multiple platforms (macOS/Linux on ARM64/AMD64)
make test # Run all tests
make clean # Clean build artifacts
make dev # Quick build + run evaluation preview
make install # Copy binary to ~/.local/binThe build system includes several optimisations:
- Compiler flags:
-s -w -trimpathremove debug symbols and build paths - UPX compression: Automatically applied in
build-releaseandcross-compileif UPX is installed - Cross-platform: The Makefile handles UPX platform differences.
- NOTE: UPX compression for macOS is officially unsupported until further notice.
Size comparison (typical results as of v0.4.0):
- Default Go build: ~6MB
- Optimised build: ~4MB (31% reduction)
- With UPX: ~1.6MB (74% reduction)
- PowerShell on macOS: prior versions injected
/etc/pathsand/etc/paths.d/*as Pathuni entries by default. The default is nowinclude_system_paths_as: system, which renders them as System ([.]), unaffected by tag filters, and controlled by-p system|all. If you relied on tag filtering for these paths, setinclude_system_paths_as: pathuniand optionally providetags:.
