mirror of
https://github.com/robbyrussell/oh-my-zsh.git
synced 2026-09-28 05:56:07 +02:00
Compare commits
98
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78c871176f | ||
|
|
1595e00f7c | ||
|
|
eb62826522 | ||
|
|
c15b32baa4 | ||
|
|
ffd4c95a4f | ||
|
|
4cdd45338c | ||
|
|
8e5be2cb3a | ||
|
|
97e11051e2 | ||
|
|
178813b7b9 | ||
|
|
0e98520c5b | ||
|
|
b54a719775 | ||
|
|
1f9511f132 | ||
|
|
97b27bb2ec | ||
|
|
99aaf58d00 | ||
|
|
6f39351fef | ||
|
|
069512d14a | ||
|
|
0912e05c05 | ||
|
|
076d692dac | ||
|
|
7e2aa2c8cb | ||
|
|
ab84cca50a | ||
|
|
a4a224b3fe | ||
|
|
898b579cf5 | ||
|
|
bc0d7fa6ab | ||
|
|
ed19f8ffcf | ||
|
|
26b4e0a8c0 | ||
|
|
a8afe14b93 | ||
|
|
be0dfaad6b | ||
|
|
14200130f0 | ||
|
|
ad586ffeca | ||
|
|
da368d6468 | ||
|
|
6adfef3a8b | ||
|
|
c5ba74cf02 | ||
|
|
7ea697fd81 | ||
|
|
d26b91b3fe | ||
|
|
64c49681f1 | ||
|
|
f5f46df291 | ||
|
|
9e67851a2f | ||
|
|
2ea792fd9b | ||
|
|
bce9fec859 | ||
|
|
12eb42de60 | ||
|
|
f25287d881 | ||
|
|
b37dd49ca5 | ||
|
|
e1d1f0dcd5 | ||
|
|
59a9740721 | ||
|
|
98fe9b81a6 | ||
|
|
f2546022a5 | ||
|
|
677a4592b1 | ||
|
|
8c8782e362 | ||
|
|
51e98fadc9 | ||
|
|
19962acc06 | ||
|
|
ff2f16e8df | ||
|
|
81becab1e7 | ||
|
|
65749801cf | ||
|
|
d2379b2701 | ||
|
|
ff1df9a039 | ||
|
|
639b566f0e | ||
|
|
df34d2b8d5 | ||
|
|
96ea17080a | ||
|
|
0a91ce20d5 | ||
|
|
9a67e3b3f5 | ||
|
|
d708ca9d99 | ||
|
|
ffa8487bc7 | ||
|
|
3f6f72010f | ||
|
|
5181447da8 | ||
|
|
c954bbb168 | ||
|
|
630a7c04c3 | ||
|
|
e25f96735e | ||
|
|
70ad5e3df8 | ||
|
|
b86a99da17 | ||
|
|
cfdc4822d4 | ||
|
|
c86ba78e2f | ||
|
|
d170d18746 | ||
|
|
c90141ed77 | ||
|
|
8eff9a5455 | ||
|
|
5ddb7fedcc | ||
|
|
ddcdc26692 | ||
|
|
fb03e414ee | ||
|
|
b26b500263 | ||
|
|
fe11a3ae23 | ||
|
|
87a9b16b72 | ||
|
|
857a646516 | ||
|
|
5ef67beadb | ||
|
|
d633270784 | ||
|
|
43c68566be | ||
|
|
cb64103161 | ||
|
|
0eecd2821e | ||
|
|
2a5313a9d0 | ||
|
|
24959d5817 | ||
|
|
efd6135bf5 | ||
|
|
c68e752b1b | ||
|
|
8d0ff417c3 | ||
|
|
80a6f169c7 | ||
|
|
c4e39ad56b | ||
|
|
7478f1fd22 | ||
|
|
a07126330b | ||
|
|
1381fec0bf | ||
|
|
3604dc23e0 | ||
|
|
8ab16fa493 |
@@ -6,9 +6,23 @@ updates:
|
||||
interval: "weekly"
|
||||
day: "sunday"
|
||||
labels: []
|
||||
groups:
|
||||
gha-minor:
|
||||
patterns:
|
||||
- "*"
|
||||
update-types:
|
||||
- minor
|
||||
- patch
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/.github/workflows/dependencies"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "sunday"
|
||||
labels: []
|
||||
groups:
|
||||
py-minor:
|
||||
patterns:
|
||||
- "*"
|
||||
update-types:
|
||||
- minor
|
||||
- patch
|
||||
|
||||
@@ -12,7 +12,7 @@ dependencies:
|
||||
plugins/gradle:
|
||||
repo: gradle/gradle-completion
|
||||
branch: master
|
||||
version: d8bc301a1fdeed8dc1dd9675138e1d9b7ddc5e4e
|
||||
version: 63578c9bc56134b80766e88957ba0239e6be2343
|
||||
precopy: |
|
||||
set -e
|
||||
find . ! -name _gradle ! -name LICENSE -delete
|
||||
@@ -30,7 +30,7 @@ dependencies:
|
||||
plugins/kube-ps1:
|
||||
repo: jonmosco/kube-ps1
|
||||
branch: master
|
||||
version: e19c9ee867c5655814c384a6bf543e330e6ef1b7
|
||||
version: 04af46f7fe888ad287abcab6699b9bb10234bc3d
|
||||
precopy: |
|
||||
set -e
|
||||
find . ! -name kube-ps1.sh ! -name LICENSE ! -name README.md -delete
|
||||
@@ -46,9 +46,10 @@ dependencies:
|
||||
plugins/z:
|
||||
branch: master
|
||||
repo: agkozak/zsh-z
|
||||
version: acd0e1984df350c189f8f9c4956ec586b6c73fca
|
||||
version: 102fb78036ed76feedf623907483691777a1d510
|
||||
precopy: |
|
||||
set -e
|
||||
rm -r tests
|
||||
test -e README.md && mv -f README.md MANUAL.md
|
||||
postcopy: |
|
||||
set -e
|
||||
|
||||
@@ -16,22 +16,22 @@ jobs:
|
||||
contents: write # this is needed to push commits and branches
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@a5ad31d6a139d249332a2605b85202e8c0b78450 # v2.19.1
|
||||
uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Authenticate as @ohmyzsh
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||
with:
|
||||
client-id: ${{ secrets.OHMYZSH_CLIENT_ID }}
|
||||
private-key: ${{ secrets.OHMYZSH_APP_PRIVATE_KEY }}
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
cache: "pip"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
certifi==2026.4.22
|
||||
charset-normalizer==3.4.7
|
||||
idna==3.13
|
||||
certifi==2026.7.22
|
||||
charset-normalizer==3.5.0
|
||||
idna==3.18
|
||||
PyYAML==6.0.3
|
||||
requests==2.33.1
|
||||
requests==2.34.2
|
||||
semver==3.0.4
|
||||
urllib3==2.6.3
|
||||
urllib3==2.7.0
|
||||
|
||||
@@ -219,6 +219,7 @@ class Dependency:
|
||||
if status["has_updates"] is True:
|
||||
short_sha = status["head_ref"][:8]
|
||||
new_version = status["version"] if is_tag else short_sha
|
||||
source_ref = new_version if is_tag else status["head_ref"]
|
||||
|
||||
try:
|
||||
branch_name = f"update/{self.path}/{new_version}"
|
||||
@@ -227,7 +228,7 @@ class Dependency:
|
||||
branch = Git.checkout_or_create_branch(branch_name)
|
||||
|
||||
# Update dependency files
|
||||
self.__apply_upstream_changes()
|
||||
self.__apply_upstream_changes(source_ref)
|
||||
|
||||
if not Git.repo_is_clean():
|
||||
# Update dependencies.yml file
|
||||
@@ -297,7 +298,7 @@ Check out the [list of changes]({status["compare_url"]}).
|
||||
dep_yaml = DependencyStore.update_dependency_version(self.path, new_version)
|
||||
DependencyStore.write_store(DEPS_YAML_FILE, dep_yaml)
|
||||
|
||||
def __apply_upstream_changes(self) -> None:
|
||||
def __apply_upstream_changes(self, ref: str) -> None:
|
||||
# Patterns to ignore in copying files from upstream repo
|
||||
GLOBAL_IGNORE = [".git", ".github", ".gitignore"]
|
||||
|
||||
@@ -306,12 +307,11 @@ Check out the [list of changes]({status["compare_url"]}).
|
||||
postcopy = self.values.get("postcopy")
|
||||
|
||||
repo = self.values["repo"]
|
||||
branch = self.values["branch"]
|
||||
remote_url = f"https://github.com/{repo}.git"
|
||||
repo_dir = os.path.join(TMP_DIR, repo)
|
||||
|
||||
# Clone repository
|
||||
Git.clone(remote_url, branch, repo_dir, reclone=True)
|
||||
Git.clone(remote_url, ref, repo_dir, reclone=True)
|
||||
|
||||
# Run precopy on tmp repo
|
||||
if precopy is not None:
|
||||
@@ -348,7 +348,7 @@ class Git:
|
||||
default_branch = "master"
|
||||
|
||||
@staticmethod
|
||||
def clone(remote_url: str, branch: str, repo_dir: str, reclone=False):
|
||||
def clone(remote_url: str, ref: str, repo_dir: str, reclone=False):
|
||||
# If repo needs to be fresh
|
||||
if reclone and os.path.exists(repo_dir):
|
||||
shutil.rmtree(repo_dir)
|
||||
@@ -356,11 +356,11 @@ class Git:
|
||||
# Clone repo in tmp directory and checkout branch
|
||||
if not os.path.exists(repo_dir):
|
||||
print(
|
||||
f"Cloning {remote_url} to {repo_dir} and checking out {branch}",
|
||||
f"Cloning {remote_url} to {repo_dir} and checking out {ref}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
CommandRunner.run_or_fail(
|
||||
["git", "clone", "--depth=1", "-b", branch, remote_url, repo_dir],
|
||||
["git", "clone", "--depth=1", "--revision", ref, remote_url, repo_dir],
|
||||
stage="Clone",
|
||||
)
|
||||
|
||||
@@ -392,13 +392,15 @@ class Git:
|
||||
Returns `False` if the repo is dirty.
|
||||
"""
|
||||
try:
|
||||
CommandRunner.run_or_fail(
|
||||
["git", "diff", "--exit-code"], stage="CheckRepoClean"
|
||||
result = CommandRunner.run_or_fail(
|
||||
["git", "status", "--porcelain", "--untracked-files=normal"],
|
||||
stage="CheckRepoClean",
|
||||
)
|
||||
return True
|
||||
except CommandRunner.Exception:
|
||||
return False
|
||||
|
||||
return result.stdout.strip() == b""
|
||||
|
||||
@staticmethod
|
||||
def add_and_commit(scope: str, version: str) -> bool:
|
||||
"""
|
||||
|
||||
@@ -26,12 +26,12 @@ jobs:
|
||||
- macos-latest
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@a5ad31d6a139d249332a2605b85202e8c0b78450 # v2.19.1
|
||||
uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Set up git repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Install zsh
|
||||
if: runner.os == 'Linux'
|
||||
run: sudo apt-get update; sudo apt-get install zsh
|
||||
@@ -47,12 +47,12 @@ jobs:
|
||||
- test
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@a5ad31d6a139d249332a2605b85202e8c0b78450 # v2.19.1
|
||||
uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Install Vercel CLI
|
||||
run: npm install -g vercel
|
||||
- name: Setup project and deploy
|
||||
|
||||
@@ -24,12 +24,12 @@ jobs:
|
||||
if: github.repository == 'ohmyzsh/ohmyzsh'
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@a5ad31d6a139d249332a2605b85202e8c0b78450 # v2.19.1
|
||||
uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Set up git repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Install zsh
|
||||
run: sudo apt-get update; sudo apt-get install zsh
|
||||
- name: Check syntax
|
||||
@@ -41,3 +41,49 @@ jobs:
|
||||
./themes/*.zsh-theme; do
|
||||
zsh -n "$file" || return 1
|
||||
done
|
||||
|
||||
- name: Focused syntax check for bootstrap-adjacent touched files
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
mapfile -t touched < <(
|
||||
git diff --name-only "${{ github.event.pull_request.base.sha }}" "${{ github.sha }}" \
|
||||
| grep -E '^(lib/bootstrap\.zsh|lib/tests/bootstrap.*\.zsh|oh-my-zsh\.sh|lib/(completion|compfix|functions|theme-and-appearance)\.zsh|plugins/.+/.+\.plugin\.zsh|themes/.+\.zsh-theme)$' \
|
||||
|| true
|
||||
)
|
||||
|
||||
if [ "${#touched[@]}" -eq 0 ]; then
|
||||
echo "No bootstrap-adjacent files touched"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for file in "${touched[@]}"; do
|
||||
echo "Syntax checking $file"
|
||||
zsh -n "$file"
|
||||
done
|
||||
|
||||
- name: Run bootstrap unit test
|
||||
if: github.event_name == 'pull_request'
|
||||
run: zsh ./lib/tests/bootstrap.test.zsh
|
||||
|
||||
- name: Run inline bootstrap invariants smoke test
|
||||
if: github.event_name == 'pull_request'
|
||||
run: zsh ./lib/tests/bootstrap-inline-ci.test.zsh
|
||||
|
||||
plugin-manager-smoke:
|
||||
name: Plugin manager smoke (${{ matrix.manager }}:${{ matrix.scenario }})
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'ohmyzsh/ohmyzsh' && github.event_name == 'pull_request'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
manager: [antigen, zinit]
|
||||
scenario: [positive, negative]
|
||||
steps:
|
||||
- name: Set up git repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Install zsh
|
||||
run: sudo apt-get update; sudo apt-get install zsh
|
||||
|
||||
- name: Run plugin-manager bootstrap integration
|
||||
run: zsh ./lib/tests/plugin-manager-bootstrap-integration.test.zsh "${{ matrix.manager }}" "${{ matrix.scenario }}"
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
name: Plugin manager integration (advisory)
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 6 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
plugin-manager-matrix:
|
||||
name: ${{ matrix.manager }}:${{ matrix.scenario }}
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'ohmyzsh/ohmyzsh'
|
||||
continue-on-error: true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
manager: [antigen, zinit, zgen, zplug, antibody, zulu]
|
||||
scenario: [positive, negative]
|
||||
|
||||
steps:
|
||||
- name: Set up git repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Install zsh and go
|
||||
run: sudo apt-get update; sudo apt-get install zsh golang
|
||||
|
||||
- name: Run plugin-manager bootstrap integration
|
||||
run: zsh ./lib/tests/plugin-manager-bootstrap-integration.test.zsh "${{ matrix.manager }}" "${{ matrix.scenario }}"
|
||||
|
||||
- name: Rollout note
|
||||
if: always()
|
||||
run: |
|
||||
echo "Phase 2 active: full manager matrix is advisory/nightly."
|
||||
echo "Phase 3 target: promote full matrix to required after stability window."
|
||||
@@ -17,12 +17,12 @@ jobs:
|
||||
if: github.repository == 'ohmyzsh/ohmyzsh'
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@a5ad31d6a139d249332a2605b85202e8c0b78450 # v2.19.1
|
||||
uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Authenticate as @ohmyzsh
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||
with:
|
||||
client-id: ${{ secrets.OHMYZSH_CLIENT_ID }}
|
||||
private-key: ${{ secrets.OHMYZSH_APP_PRIVATE_KEY }}
|
||||
|
||||
@@ -36,17 +36,17 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@a5ad31d6a139d249332a2605b85202e8c0b78450 # v2.19.1
|
||||
uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: "Run analysis"
|
||||
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
|
||||
uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
@@ -60,6 +60,6 @@ jobs:
|
||||
retention-days: 5
|
||||
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
||||
uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
@@ -23,6 +23,7 @@ you would make is not already covered.
|
||||
- [A note on AI-assisted contributions](#a-note-on-ai-assisted-contributions)
|
||||
- [Use the Search, Luke](#use-the-search-luke)
|
||||
- [Commit Guidelines](#commit-guidelines)
|
||||
- [Plugin And Theme Bootstrap Compatibility](#plugin-and-theme-bootstrap-compatibility)
|
||||
- [Format](#format)
|
||||
- [Style](#style)
|
||||
- [Volunteer](#volunteer)
|
||||
@@ -176,6 +177,24 @@ specification. The automatic changelog tool uses these to automatically generate
|
||||
a changelog based on the commit messages. Here's a guide to writing a commit message
|
||||
to allow this:
|
||||
|
||||
### Plugin And Theme Bootstrap Compatibility
|
||||
|
||||
Oh My Zsh now provides a shared bootstrap entrypoint at `lib/bootstrap.zsh` for plugin-manager setups that don't
|
||||
source `oh-my-zsh.sh`.
|
||||
|
||||
When working on plugins/themes:
|
||||
|
||||
- assume bootstrap has already run in manager-driven setups
|
||||
- rely on bootstrap guarantees (`ZSH*` defaults, cache/completions setup, required `fpath` entries)
|
||||
- avoid adding per-plugin/per-theme copies of common setup unless truly plugin-specific
|
||||
|
||||
Common patterns we should migrate away from over time:
|
||||
|
||||
- repeated cache setup logic (`mkdir -p "$ZSH_CACHE_DIR/completions"`)
|
||||
- repeated base `fpath` bootstrapping for OMZ core/custom directories
|
||||
|
||||
If your plugin/theme still needs custom initialization, keep it scoped to that plugin/theme behavior only.
|
||||
|
||||
### Format
|
||||
|
||||
```
|
||||
|
||||
@@ -46,6 +46,7 @@ Twitter), and join us on [Discord](https://discord.gg/ohmyzsh).
|
||||
- [Manual Installation](#manual-installation)
|
||||
- [Installation Problems](#installation-problems)
|
||||
- [Custom Plugins And Themes](#custom-plugins-and-themes)
|
||||
- [Using Plugin Managers Without `oh-my-zsh.sh`](#using-plugin-managers-without-oh-my-zshsh)
|
||||
- [Enable GNU ls In macOS And FreeBSD Systems](#enable-gnu-ls-in-macos-and-freebsd-systems)
|
||||
- [Skip Aliases](#skip-aliases)
|
||||
- [Async git prompt](#async-git-prompt)
|
||||
@@ -349,6 +350,120 @@ If you have many functions that go well together, you can put them as a `XYZ.plu
|
||||
If you would like to override the functionality of a plugin distributed with Oh My Zsh, create a plugin of the
|
||||
same name in the `custom/plugins/` directory and it will be loaded instead of the one in `plugins/`.
|
||||
|
||||
### Using Plugin Managers Without `oh-my-zsh.sh`
|
||||
|
||||
Some plugin managers load individual OMZ plugins/themes directly and do not source `oh-my-zsh.sh`.
|
||||
For those setups, source this bootstrap entrypoint before loading any OMZ plugin or theme:
|
||||
|
||||
```zsh
|
||||
source "$ZSH/lib/bootstrap.zsh"
|
||||
```
|
||||
|
||||
#### Bootstrap compatibility contract
|
||||
|
||||
`lib/bootstrap.zsh` is idempotent and is the only required pre-load step for plugin-manager setups.
|
||||
It guarantees:
|
||||
|
||||
- `ZSH`, `ZSH_CUSTOM`, and `ZSH_CACHE_DIR` defaults
|
||||
- writable cache fallback to `${XDG_CACHE_HOME:-$HOME/.cache}/oh-my-zsh`
|
||||
- `$ZSH_CACHE_DIR/completions` directory creation
|
||||
- required OMZ `fpath` entries for plugins/themes:
|
||||
- `$ZSH/functions`
|
||||
- `$ZSH/completions`
|
||||
- `$ZSH_CUSTOM/functions`
|
||||
- `$ZSH_CUSTOM/completions`
|
||||
- `$ZSH_CACHE_DIR/completions`
|
||||
- public signal: `OMZ_IS_BOOTSTRAPPED=true` (for one-time hook guards)
|
||||
|
||||
What still requires `oh-my-zsh.sh`:
|
||||
|
||||
- update checks
|
||||
- plugin auto-discovery from the `plugins=(...)` list
|
||||
- `compinit`/`compfix` orchestration and zcompdump metadata refresh
|
||||
- alias filtering via `:omz:*` zstyles during file sourcing
|
||||
- final full-init signal: `OMZ_IS_LOADED=true`
|
||||
|
||||
#### Plugin manager hook guidance
|
||||
|
||||
Run bootstrap once, before any OMZ plugin/theme load:
|
||||
|
||||
```zsh
|
||||
[[ -n "${OMZ_IS_BOOTSTRAPPED:-}" ]] || source "$ZSH/lib/bootstrap.zsh"
|
||||
```
|
||||
|
||||
- **Antigen** (pre-bundle hook pattern):
|
||||
|
||||
```zsh
|
||||
omz_preload() { [[ -n "${OMZ_IS_BOOTSTRAPPED:-}" ]] || source "$ZSH/lib/bootstrap.zsh"; }
|
||||
omz_preload
|
||||
antigen bundle OMZ::plugins/git
|
||||
antigen apply
|
||||
```
|
||||
|
||||
- **Zinit** (`atinit` pre-load hook):
|
||||
|
||||
```zsh
|
||||
zinit ice atinit'[[ -n "${OMZ_IS_BOOTSTRAPPED:-}" ]] || source "$ZSH/lib/bootstrap.zsh"'
|
||||
zinit snippet OMZ::plugins/git/git.plugin.zsh
|
||||
```
|
||||
|
||||
- **zgen** (pre-load hook function called before OMZ loads):
|
||||
|
||||
```zsh
|
||||
omz_preload() { [[ -n "${OMZ_IS_BOOTSTRAPPED:-}" ]] || source "$ZSH/lib/bootstrap.zsh"; }
|
||||
omz_preload
|
||||
zgen load ohmyzsh/ohmyzsh plugins/git
|
||||
```
|
||||
|
||||
- **zplug** (run pre-load hook immediately before OMZ entries):
|
||||
|
||||
```zsh
|
||||
omz_preload() { [[ -n "${OMZ_IS_BOOTSTRAPPED:-}" ]] || source "$ZSH/lib/bootstrap.zsh"; }
|
||||
omz_preload
|
||||
zplug "plugins/git", from:oh-my-zsh
|
||||
zplug load
|
||||
```
|
||||
|
||||
- **antibody** (run pre-load hook before `antibody bundle`/`source` output):
|
||||
|
||||
```zsh
|
||||
omz_preload() { [[ -n "${OMZ_IS_BOOTSTRAPPED:-}" ]] || source "$ZSH/lib/bootstrap.zsh"; }
|
||||
omz_preload
|
||||
source <(antibody bundle <<'EOF'
|
||||
ohmyzsh/ohmyzsh path:plugins/git
|
||||
EOF
|
||||
)
|
||||
```
|
||||
|
||||
- **zulu** (run pre-load hook before OMZ module loads):
|
||||
|
||||
```zsh
|
||||
omz_preload() { [[ -n "${OMZ_IS_BOOTSTRAPPED:-}" ]] || source "$ZSH/lib/bootstrap.zsh"; }
|
||||
omz_preload
|
||||
zulu install oh-my-zsh
|
||||
```
|
||||
|
||||
Compatibility matrix:
|
||||
|
||||
| Load mode | `OMZ_IS_BOOTSTRAPPED` | `OMZ_IS_LOADED` | Suitable for |
|
||||
| :-- | :--: | :--: | :-- |
|
||||
| `source $ZSH/oh-my-zsh.sh` | ✅ | ✅ | Full OMZ framework features |
|
||||
| `source $ZSH/lib/bootstrap.zsh` + manager-loaded OMZ plugins/themes | ✅ | ❌ | Plugin/theme prerequisites only |
|
||||
|
||||
#### Manager entrypoints and OMZ-side auto-injection limits
|
||||
|
||||
Most plugin managers load OMZ by directly sourcing selected plugin/theme/lib entrypoints, not by sourcing
|
||||
`oh-my-zsh.sh`, so OMZ cannot universally inject bootstrap on its own.
|
||||
|
||||
| Manager | Typical OMZ entrypoint(s) it loads | Can OMZ auto-inject bootstrap without user hook? |
|
||||
| :-- | :-- | :--: |
|
||||
| Antigen | `antigen bundle ...` targets after `antigen use oh-my-zsh` | ❌ |
|
||||
| Zinit | `OMZ::`, `OMZL::`, `OMZP::`, `OMZT::` snippets | ❌ |
|
||||
| zgen | `zgen oh-my-zsh ...` selected OMZ paths | ❌ |
|
||||
| zplug | `from:oh-my-zsh` selected OMZ entries | ❌ |
|
||||
| antibody | `ohmyzsh/ohmyzsh path:...` selected OMZ paths | ❌ |
|
||||
| zulu | OMZ package/module entries selected by zulu | ❌ |
|
||||
|
||||
### Enable GNU ls In macOS And FreeBSD Systems
|
||||
|
||||
<a name="enable-gnu-ls"></a>
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
#
|
||||
# Shared Oh My Zsh bootstrap entrypoint.
|
||||
#
|
||||
# This file can be sourced by plugin managers that don't source `oh-my-zsh.sh`.
|
||||
# It is safe to source multiple times.
|
||||
#
|
||||
|
||||
# Keep source path for callers that invoke omz_bootstrap again later.
|
||||
typeset -g _OMZ_BOOTSTRAP_SOURCE="${${(%):-%x}:a}"
|
||||
|
||||
omz_bootstrap() {
|
||||
# If ZSH is not defined, infer from this file location.
|
||||
[[ -n "${ZSH:-}" ]] || export ZSH="${_OMZ_BOOTSTRAP_SOURCE:h:h}"
|
||||
|
||||
# Set ZSH_CUSTOM to the path where custom config files and plugins exist.
|
||||
[[ -n "${ZSH_CUSTOM:-}" ]] || ZSH_CUSTOM="$ZSH/custom"
|
||||
|
||||
# Set cache directory.
|
||||
[[ -n "${ZSH_CACHE_DIR:-}" ]] || ZSH_CACHE_DIR="$ZSH/cache"
|
||||
|
||||
# Ensure cache dir is writable, otherwise fallback to a HOME-based cache.
|
||||
if [[ ! -w "$ZSH_CACHE_DIR" ]]; then
|
||||
ZSH_CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/oh-my-zsh"
|
||||
fi
|
||||
|
||||
# Create cache and completions dir.
|
||||
command mkdir -p "$ZSH_CACHE_DIR/completions"
|
||||
|
||||
# Add required OMZ search paths.
|
||||
local dir
|
||||
for dir in \
|
||||
"$ZSH/functions" \
|
||||
"$ZSH/completions" \
|
||||
"$ZSH_CUSTOM/functions" \
|
||||
"$ZSH_CUSTOM/completions" \
|
||||
"$ZSH_CACHE_DIR/completions"; do
|
||||
(( ${fpath[(Ie)$dir]} )) || fpath=("$dir" $fpath)
|
||||
done
|
||||
|
||||
# Public signal: OMZ bootstrap has completed.
|
||||
typeset -g OMZ_IS_BOOTSTRAPPED=true
|
||||
}
|
||||
|
||||
omz_bootstrap "$@"
|
||||
+1
-1
@@ -371,7 +371,7 @@ multi == 1 && /^[^#]*\)/ {
|
||||
next
|
||||
}
|
||||
|
||||
# if multi flag is enabled and we didnt find a closing parenthesis,
|
||||
# if multi flag is enabled and we didn't find a closing parenthesis,
|
||||
# get the indentation level to match when adding plugins
|
||||
multi == 1 && /^[^#]*/ {
|
||||
indent=\"\"
|
||||
|
||||
+11
-2
@@ -40,6 +40,15 @@ function _omz_git_prompt_info() {
|
||||
}
|
||||
|
||||
function _omz_git_prompt_status() {
|
||||
# OHMYZSH-13330: avoid "regex matching error: illegal byte sequence".
|
||||
# zsh's "=~" operator delegates to the C library regex, which aborts with
|
||||
# REG_ILLSEQ when the subject contains an invalid byte sequence under a
|
||||
# multibyte locale (e.g. a filename with non-UTF-8 bytes in `git status`
|
||||
# output). Forcing the C locale makes every byte a valid character, so the
|
||||
# regex matching below never fails that way. `git status --porcelain` is
|
||||
# locale-independent, so this does not change the parsed output.
|
||||
local -x LC_ALL=C
|
||||
|
||||
[[ "$(__git_prompt_git config --get oh-my-zsh.hide-status 2>/dev/null)" = 1 ]] && return
|
||||
|
||||
# Maps a git status prefix to an internal constant
|
||||
@@ -162,13 +171,13 @@ if zstyle -t ':omz:alpha:lib:git' async-prompt \
|
||||
# or any of the other prompt variables
|
||||
function _defer_async_git_register() {
|
||||
# Check if git_prompt_info is used in a prompt variable
|
||||
case "${PS1}:${PS2}:${PS3}:${PS4}:${RPROMPT}:${RPS1}:${RPS2}:${RPS3}:${RPS4}" in
|
||||
case "${PS1}:${PS2}:${PS3}:${PS4}:${RPROMPT-}:${RPS1-}:${RPS2-}:${RPS3-}:${RPS4-}" in
|
||||
*(\$\(git_prompt_info\)|\`git_prompt_info\`)*)
|
||||
_omz_register_handler _omz_git_prompt_info
|
||||
;;
|
||||
esac
|
||||
|
||||
case "${PS1}:${PS2}:${PS3}:${PS4}:${RPROMPT}:${RPS1}:${RPS2}:${RPS3}:${RPS4}" in
|
||||
case "${PS1}:${PS2}:${PS3}:${PS4}:${RPROMPT-}:${RPS1-}:${RPS2-}:${RPS3-}:${RPS4-}" in
|
||||
*(\$\(git_prompt_status\)|\`git_prompt_status\`)*)
|
||||
_omz_register_handler _omz_git_prompt_status
|
||||
;;
|
||||
|
||||
Executable
+35
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/zsh -df
|
||||
|
||||
set -eu
|
||||
|
||||
bootstrap_file="${0:A:h:h}/bootstrap.zsh"
|
||||
|
||||
assert() {
|
||||
local condition="$1" message="$2"
|
||||
if ! eval "$condition"; then
|
||||
print -u2 "\e[31mError\e[0m: $message"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
tmp==(:)
|
||||
mkdir -p "$tmp/ohmyzsh"/{functions,completions,cache} "$tmp/ohmyzsh/custom"/{functions,completions}
|
||||
|
||||
export ZSH="$tmp/ohmyzsh"
|
||||
unset ZSH_CUSTOM ZSH_CACHE_DIR OMZ_IS_BOOTSTRAPPED
|
||||
fpath=()
|
||||
|
||||
source "$bootstrap_file"
|
||||
|
||||
assert '[[ "$OMZ_IS_BOOTSTRAPPED" == true ]]' "OMZ bootstrap signal should be true"
|
||||
assert '[[ -d "$ZSH_CACHE_DIR/completions" ]]' "cache completions directory should exist"
|
||||
assert '[[ "${fpath[(Ie)$ZSH/functions]}" -gt 0 ]]' "fpath should include OMZ functions"
|
||||
assert '[[ "${fpath[(Ie)$ZSH/completions]}" -gt 0 ]]' "fpath should include OMZ completions"
|
||||
assert '[[ "${fpath[(Ie)$ZSH_CUSTOM/functions]}" -gt 0 ]]' "fpath should include custom functions"
|
||||
assert '[[ "${fpath[(Ie)$ZSH_CUSTOM/completions]}" -gt 0 ]]' "fpath should include custom completions"
|
||||
assert '[[ "${fpath[(Ie)$ZSH_CACHE_DIR/completions]}" -gt 0 ]]' "fpath should include cache completions"
|
||||
|
||||
touch "$ZSH_CACHE_DIR/completions/_bootstrap_ci_smoke"
|
||||
assert '[[ -f "$ZSH_CACHE_DIR/completions/_bootstrap_ci_smoke" ]]' "completion cache write should succeed"
|
||||
|
||||
print -u2 "\e[32mSuccess\e[0m bootstrap inline invariants"
|
||||
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/zsh -df
|
||||
|
||||
set -u
|
||||
|
||||
bootstrap_file="${0:A:h:h}/bootstrap.zsh"
|
||||
|
||||
_assert() {
|
||||
local condition="$1" message="$2"
|
||||
if ! eval "$condition"; then
|
||||
print -u2 "\e[31mError\e[0m: $message"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
test_bootstrap_sets_defaults_and_paths() {
|
||||
local tmp==(:)
|
||||
mkdir -p "$tmp/ohmyzsh"/{functions,completions,cache} "$tmp/ohmyzsh/custom"/{functions,completions}
|
||||
|
||||
(
|
||||
set -e
|
||||
export ZSH="$tmp/ohmyzsh"
|
||||
unset ZSH_CUSTOM ZSH_CACHE_DIR OMZ_IS_BOOTSTRAPPED
|
||||
fpath=()
|
||||
source "$bootstrap_file"
|
||||
|
||||
_assert '[[ "$ZSH_CUSTOM" == "'"$tmp/ohmyzsh/custom"'" ]]' "ZSH_CUSTOM default should be set"
|
||||
_assert '[[ "$ZSH_CACHE_DIR" == "'"$tmp/ohmyzsh/cache"'" ]]' "ZSH_CACHE_DIR default should be set"
|
||||
_assert '[[ -d "'"$tmp/ohmyzsh/cache/completions"'" ]]' "completions dir should be created"
|
||||
_assert '[[ "${fpath[(Ie)'"$tmp/ohmyzsh/functions"']}" -gt 0 ]]' "fpath should include OMZ functions dir"
|
||||
_assert '[[ "${fpath[(Ie)'"$tmp/ohmyzsh/completions"']}" -gt 0 ]]' "fpath should include OMZ completions dir"
|
||||
_assert '[[ "${fpath[(Ie)'"$tmp/ohmyzsh/custom/functions"']}" -gt 0 ]]' "fpath should include custom functions dir"
|
||||
_assert '[[ "${fpath[(Ie)'"$tmp/ohmyzsh/custom/completions"']}" -gt 0 ]]' "fpath should include custom completions dir"
|
||||
_assert '[[ "${fpath[(Ie)'"$tmp/ohmyzsh/cache/completions"']}" -gt 0 ]]' "fpath should include cache completions dir"
|
||||
_assert '[[ "$OMZ_IS_BOOTSTRAPPED" == true ]]' "bootstrap signal should be set"
|
||||
)
|
||||
}
|
||||
|
||||
test_bootstrap_is_idempotent() {
|
||||
local tmp==(:)
|
||||
mkdir -p "$tmp/ohmyzsh"/{functions,completions,cache} "$tmp/ohmyzsh/custom"/{functions,completions}
|
||||
|
||||
(
|
||||
set -e
|
||||
export ZSH="$tmp/ohmyzsh"
|
||||
unset ZSH_CUSTOM ZSH_CACHE_DIR OMZ_IS_BOOTSTRAPPED
|
||||
fpath=()
|
||||
source "$bootstrap_file"
|
||||
source "$bootstrap_file"
|
||||
omz_bootstrap
|
||||
|
||||
_assert '[[ ${#fpath} -eq ${#${(u)fpath}} ]]' "fpath entries should not duplicate after repeated bootstrap"
|
||||
_assert '[[ "$OMZ_IS_BOOTSTRAPPED" == true ]]' "bootstrap signal should remain true"
|
||||
)
|
||||
}
|
||||
|
||||
test_bootstrap_uses_writable_cache_fallback() {
|
||||
local tmp==(:)
|
||||
mkdir -p "$tmp/ohmyzsh"/{functions,completions} "$tmp/ohmyzsh/custom"/{functions,completions}
|
||||
mkdir -p "$tmp/no-write" "$tmp/xdg-cache"
|
||||
chmod 0555 "$tmp/no-write"
|
||||
|
||||
(
|
||||
set -e
|
||||
export ZSH="$tmp/ohmyzsh"
|
||||
export XDG_CACHE_HOME="$tmp/xdg-cache"
|
||||
export ZSH_CACHE_DIR="$tmp/no-write"
|
||||
unset ZSH_CUSTOM OMZ_IS_BOOTSTRAPPED
|
||||
fpath=()
|
||||
source "$bootstrap_file"
|
||||
|
||||
_assert '[[ "$ZSH_CACHE_DIR" == "'"$tmp/xdg-cache/oh-my-zsh"'" ]]' "cache dir should fallback when not writable"
|
||||
_assert '[[ -d "'"$tmp/xdg-cache/oh-my-zsh/completions"'" ]]' "fallback completions dir should be created"
|
||||
_assert '[[ "${fpath[(Ie)'"$tmp/xdg-cache/oh-my-zsh/completions"']}" -gt 0 ]]' "fpath should include fallback completions dir"
|
||||
)
|
||||
}
|
||||
|
||||
tests=(
|
||||
test_bootstrap_sets_defaults_and_paths
|
||||
test_bootstrap_is_idempotent
|
||||
test_bootstrap_uses_writable_cache_fallback
|
||||
)
|
||||
|
||||
for test_name in $tests; do
|
||||
print -u2 "Test: $test_name"
|
||||
"$test_name" || exit 1
|
||||
print -u2 "\e[32mSuccess\e[0m"
|
||||
print -u2 ""
|
||||
done
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/zsh -df
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if (( $# != 2 )); then
|
||||
print -u2 "Usage: $0 <manager> <positive|negative>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
manager="$1"
|
||||
scenario="$2"
|
||||
|
||||
if [[ "$scenario" != "positive" && "$scenario" != "negative" ]]; then
|
||||
print -u2 "scenario must be 'positive' or 'negative'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
repo_root="${0:A:h:h:h}"
|
||||
tmp==(:)
|
||||
workspace="$tmp/workspace"
|
||||
home_dir="$tmp/home"
|
||||
cache_dir="$tmp/cache"
|
||||
zshrc="$tmp/.zshrc"
|
||||
|
||||
mkdir -p "$workspace" "$home_dir" "$cache_dir"
|
||||
|
||||
manager_install=''
|
||||
manager_source=''
|
||||
|
||||
case "$manager" in
|
||||
antigen)
|
||||
manager_install='git clone https://github.com/zsh-users/antigen.git "$MANAGER_HOME/antigen" >/dev/null 2>&1'
|
||||
manager_source='source "$MANAGER_HOME/antigen/antigen.zsh"'
|
||||
;;
|
||||
zinit)
|
||||
manager_install='git clone https://github.com/zdharma-continuum/zinit.git "$MANAGER_HOME/zinit" >/dev/null 2>&1'
|
||||
manager_source='source "$MANAGER_HOME/zinit/zinit.zsh"'
|
||||
;;
|
||||
zgen)
|
||||
manager_install='git clone https://github.com/tarjoilija/zgen.git "$MANAGER_HOME/zgen" >/dev/null 2>&1'
|
||||
manager_source='source "$MANAGER_HOME/zgen/zgen.zsh"'
|
||||
;;
|
||||
zplug)
|
||||
manager_install='git clone https://github.com/zplug/zplug "$MANAGER_HOME/zplug" >/dev/null 2>&1'
|
||||
manager_source='export ZPLUG_HOME="$MANAGER_HOME/zplug"; source "$ZPLUG_HOME/init.zsh"'
|
||||
;;
|
||||
antibody)
|
||||
manager_install='command -v go >/dev/null 2>&1; export GOBIN="$MANAGER_HOME/bin"; mkdir -p "$GOBIN"; go install github.com/getantibody/antibody@latest >/dev/null 2>&1'
|
||||
manager_source='"$MANAGER_HOME/bin/antibody" --version >/dev/null 2>&1'
|
||||
;;
|
||||
zulu)
|
||||
manager_install='git clone https://github.com/zulu-zsh/zulu.git "$MANAGER_HOME/zulu" >/dev/null 2>&1'
|
||||
manager_source='source "$MANAGER_HOME/zulu/zulu.zsh"'
|
||||
;;
|
||||
*)
|
||||
print -u2 "Unsupported manager: $manager"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
cat > "$zshrc" <<RC
|
||||
set -eo pipefail
|
||||
|
||||
export HOME="$home_dir"
|
||||
export XDG_CACHE_HOME="$cache_dir"
|
||||
export OMZ_ROOT="$repo_root"
|
||||
export ZSH="\$OMZ_ROOT"
|
||||
export MANAGER_HOME="$workspace/$manager"
|
||||
mkdir -p "\$MANAGER_HOME"
|
||||
|
||||
$manager_install
|
||||
$manager_source
|
||||
RC
|
||||
|
||||
if [[ "$scenario" == "positive" ]]; then
|
||||
cat >> "$zshrc" <<'RC'
|
||||
source "$OMZ_ROOT/lib/bootstrap.zsh"
|
||||
[[ "${OMZ_IS_BOOTSTRAPPED:-}" == true ]] || { print -u2 "bootstrap signal missing before OMZ load"; return 1; }
|
||||
RC
|
||||
else
|
||||
cat >> "$zshrc" <<'RC'
|
||||
[[ -z "${OMZ_IS_BOOTSTRAPPED:-}" ]] || { print -u2 "bootstrap signal unexpectedly set without hook"; return 1; }
|
||||
source "$OMZ_ROOT/lib/bootstrap.zsh"
|
||||
RC
|
||||
fi
|
||||
|
||||
cat >> "$zshrc" <<RC
|
||||
[[ "\${OMZ_IS_BOOTSTRAPPED:-}" == true ]] || { print -u2 "bootstrap signal not set before OMZ entrypoint sourcing"; return 1; }
|
||||
|
||||
source "\$OMZ_ROOT/lib/functions.zsh"
|
||||
source "\$OMZ_ROOT/plugins/git/git.plugin.zsh"
|
||||
autoload -Uz compinit
|
||||
compinit -i -d "\$ZSH_CACHE_DIR/.zcompdump"
|
||||
|
||||
source "\$OMZ_ROOT/themes/robbyrussell.zsh-theme"
|
||||
|
||||
[[ -d "\$ZSH_CACHE_DIR/completions" ]] || { print -u2 "completions cache directory missing"; return 1; }
|
||||
|
||||
touch "\$ZSH_CACHE_DIR/completions/_ci_${manager}_${scenario}" || { print -u2 "completion cache write failed"; return 1; }
|
||||
[[ -f "\$ZSH_CACHE_DIR/completions/_ci_${manager}_${scenario}" ]] || { print -u2 "completion cache file missing"; return 1; }
|
||||
RC
|
||||
|
||||
zsh -df "$zshrc"
|
||||
print -u2 "\e[32mSuccess\e[0m $manager $scenario"
|
||||
+8
-20
@@ -50,31 +50,14 @@ unset -f omz_f
|
||||
# If ZSH is not defined, use the current script's directory.
|
||||
[[ -n "$ZSH" ]] || export ZSH="${${(%):-%x}:a:h}"
|
||||
|
||||
# Set ZSH_CUSTOM to the path where your custom config files
|
||||
# and plugins exists, or else we will use the default custom/
|
||||
[[ -n "$ZSH_CUSTOM" ]] || ZSH_CUSTOM="$ZSH/custom"
|
||||
|
||||
# Set ZSH_CACHE_DIR to the path where cache files should be created
|
||||
# or else we will use the default cache/
|
||||
[[ -n "$ZSH_CACHE_DIR" ]] || ZSH_CACHE_DIR="$ZSH/cache"
|
||||
|
||||
# Make sure $ZSH_CACHE_DIR is writable, otherwise use a directory in $HOME
|
||||
if [[ ! -w "$ZSH_CACHE_DIR" ]]; then
|
||||
ZSH_CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/oh-my-zsh"
|
||||
fi
|
||||
|
||||
# Create cache and completions dir and add to $fpath
|
||||
mkdir -p "$ZSH_CACHE_DIR/completions"
|
||||
(( ${fpath[(Ie)$ZSH_CACHE_DIR/completions]} )) || fpath=("$ZSH_CACHE_DIR/completions" $fpath)
|
||||
# Shared, manager-agnostic bootstrap.
|
||||
source "$ZSH/lib/bootstrap.zsh"
|
||||
|
||||
# Check for updates on initial load...
|
||||
source "$ZSH/tools/check_for_upgrade.sh"
|
||||
|
||||
# Initializes Oh My Zsh
|
||||
|
||||
# add a function path
|
||||
fpath=($ZSH/{functions,completions} $ZSH_CUSTOM/{functions,completions} $fpath)
|
||||
|
||||
# Load all stock functions (from $fpath files) called below.
|
||||
autoload -U compaudit compinit zrecompile
|
||||
|
||||
@@ -124,9 +107,11 @@ fi
|
||||
if [[ "$ZSH_DISABLE_COMPFIX" != true ]]; then
|
||||
source "$ZSH/lib/compfix.zsh"
|
||||
# Load only from secure directories
|
||||
# Reset the flag compinit sets when -i excludes insecure entries
|
||||
unset _comp_secure
|
||||
compinit -i -d "$ZSH_COMPDUMP"
|
||||
# If completion insecurities exist, warn the user
|
||||
handle_completion_insecurities &|
|
||||
[[ "$_comp_secure" == yes ]] && handle_completion_insecurities &|
|
||||
else
|
||||
# If the user wants it, load from all found directories
|
||||
compinit -u -d "$ZSH_COMPDUMP"
|
||||
@@ -232,3 +217,6 @@ fi
|
||||
|
||||
# set completion colors to be the same as `ls`, after theme has been loaded
|
||||
[[ -z "$LS_COLORS" ]] || zstyle ':completion:*' list-colors "${(s.:.)LS_COLORS}"
|
||||
|
||||
# Public signal: full Oh My Zsh init script has completed.
|
||||
typeset -g OMZ_IS_LOADED=true
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
alias-finder() {
|
||||
local cmd=" " exact="" longer="" cheaper="" wordEnd="'{0,1}$" finder="" filter=""
|
||||
local cmd=" " exact="" longer="" cheaper="" wordEnd="'?$" finder="" filter=""
|
||||
|
||||
# build command and options
|
||||
for c in "$@"; do
|
||||
@@ -31,7 +31,7 @@ alias-finder() {
|
||||
|
||||
# find with alias and grep, removing last word each time until no more words
|
||||
while [[ $cmd != "" ]]; do
|
||||
finder="'{0,1}$cmd$wordEnd"
|
||||
finder="'?$cmd$wordEnd"
|
||||
|
||||
# make filter to find only shorter results than current cmd
|
||||
if [[ $cheaper == true ]]; then
|
||||
|
||||
@@ -6,7 +6,9 @@ import argparse
|
||||
|
||||
def parse(line):
|
||||
left = line[0:line.find('=')].strip()
|
||||
right = line[line.find('=')+1:].strip('\'"\n ')
|
||||
right = line[line.find('=')+1:].strip('\n ')
|
||||
if len(right) >= 2 and right[0] == right[-1] and right[0] in '\'"':
|
||||
right = right[1:-1]
|
||||
try:
|
||||
cmd = next(part for part in right.split() if len([char for char in '=<>' if char in part])==0)
|
||||
except StopIteration:
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# Functions
|
||||
function ansible-version(){
|
||||
function ansible-version() {
|
||||
ansible --version
|
||||
}
|
||||
|
||||
function ansible-role-init(){
|
||||
if ! [ -z $1 ] ; then
|
||||
function ansible-role-init() {
|
||||
if [[ -n "$1" ]]; then
|
||||
echo "Ansible Role : $1 Creating...."
|
||||
ansible-galaxy init $1
|
||||
tree $1
|
||||
ansible-galaxy init "$1"
|
||||
tree "$1"
|
||||
else
|
||||
echo "Usage : ansible-role-init <role name>"
|
||||
echo "Example : ansible-role-init role1"
|
||||
|
||||
@@ -21,7 +21,7 @@ plugins=(... archlinux)
|
||||
| pacloc | `pacman -Qi` | Display information about a package in the local database |
|
||||
| paclocs | `pacman -Qs` | Search for packages in the local database |
|
||||
| paclr | `sudo pacman -Scc` | Remove all files from the cache |
|
||||
| paclsorphans | `sudo pacman -Qdt` | List all orphaned packages |
|
||||
| paclsorphans | `pacman -Qdt` | List all orphaned packages |
|
||||
| pacmir | `sudo pacman -Syy` | Force refresh of all package lists after updating mirrorlist |
|
||||
| pacre | `sudo pacman -R` | Remove packages, keeping its settings and dependencies |
|
||||
| pacrem | `sudo pacman -Rns` | Remove packages, including its settings and dependencies |
|
||||
|
||||
@@ -16,7 +16,7 @@ alias pacloc='pacman -Qi'
|
||||
alias paclocs='pacman -Qs'
|
||||
alias pacinsd='sudo pacman -S --asdeps'
|
||||
alias pacmir='sudo pacman -Syy'
|
||||
alias paclsorphans='sudo pacman -Qdt'
|
||||
alias paclsorphans='pacman -Qdt'
|
||||
alias pacrmorphans='sudo pacman -Rs $(pacman -Qtdq)'
|
||||
alias pacfileupg='sudo pacman -Fy'
|
||||
alias pacfiles='pacman -F'
|
||||
|
||||
@@ -9,14 +9,14 @@ function agr() {
|
||||
# Update state file if enabled
|
||||
function _aws_update_state() {
|
||||
if [[ "$AWS_PROFILE_STATE_ENABLED" == true ]]; then
|
||||
test -d $(dirname ${AWS_STATE_FILE}) || exit 1
|
||||
test -d "$(dirname "${AWS_STATE_FILE}")" || return 1
|
||||
echo "${AWS_PROFILE} ${AWS_REGION}" > "${AWS_STATE_FILE}"
|
||||
fi
|
||||
}
|
||||
|
||||
function _aws_clear_state() {
|
||||
if [[ "$AWS_PROFILE_STATE_ENABLED" == true ]]; then
|
||||
test -d $(dirname ${AWS_STATE_FILE}) || exit 1
|
||||
test -d "$(dirname "${AWS_STATE_FILE}")" || return 1
|
||||
echo -n > "${AWS_STATE_FILE}"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Bedtools plugin
|
||||
|
||||
This plugin adds support for the [bedtools suite](http://bedtools.readthedocs.org/en/latest/):
|
||||
This plugin adds support for the [bedtools suite](https://bedtools.readthedocs.io/en/latest/):
|
||||
|
||||
* Adds autocomplete options for all bedtools sub commands.
|
||||
|
||||
@@ -17,6 +17,12 @@ If `brew` is not found in the PATH, this plugin will attempt to find it in commo
|
||||
In case you installed `brew` in a non-common location, you can still set `BREW_LOCATION` variable pointing to
|
||||
the `brew` binary before sourcing `oh-my-zsh.sh` and it'll set up the environment.
|
||||
|
||||
### sbin directory
|
||||
|
||||
This plugin also adds `$HOMEBREW_PREFIX/sbin` to the PATH if the directory exists and isn't already present.
|
||||
Some Homebrew formulae (e.g. `mtr`) install executables to `sbin`, which `brew doctor` checks for. This
|
||||
ensures the `bdr` alias runs without warnings.
|
||||
|
||||
## Aliases
|
||||
|
||||
| Alias | Command | Description |
|
||||
@@ -40,7 +46,7 @@ the `brew` binary before sourcing `oh-my-zsh.sh` and it'll set up the environmen
|
||||
| `bo` | `brew outdated` | List installed formulae that have an updated version available. |
|
||||
| `br` | `brew reinstall` | Reinstall a formula. |
|
||||
| `brewp` | `brew pin` | Pin a specified formula so that it's not upgraded. |
|
||||
| `brews` | `brew list -1` | List installed formulae or the installed files for a given formula. |
|
||||
| `brews` | _function_ | List installed leaf formulae with their dependencies, then casks. |
|
||||
| `brewsp` | `brew list --pinned` | List pinned formulae, or show the version of a given formula. |
|
||||
| `brh` | `brew reinstall --HEAD` | Reinstall a formula with --HEAD |
|
||||
| `bs` | `brew search` | Perform a substring search of cask tokens and formula names for text. |
|
||||
|
||||
@@ -30,6 +30,16 @@ if [[ -z "$HOMEBREW_PREFIX" ]]; then
|
||||
export HOMEBREW_PREFIX="$(brew --prefix)"
|
||||
fi
|
||||
|
||||
# Add Homebrew sbin to PATH if it exists and is not already in PATH.
|
||||
# Homebrew's shellenv only adds bin directories, not sbin. Some formulae
|
||||
# (e.g. mtr) install executables to sbin, and brew doctor warns if it's
|
||||
# missing from PATH.
|
||||
if [[ -d "$HOMEBREW_PREFIX/sbin" ]]; then
|
||||
if [[ ! "$PATH" == *"$HOMEBREW_PREFIX/sbin"* ]]; then
|
||||
export PATH="$HOMEBREW_PREFIX/sbin:$PATH"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -d "$HOMEBREW_PREFIX/share/zsh/site-functions" ]]; then
|
||||
fpath+=("$HOMEBREW_PREFIX/share/zsh/site-functions")
|
||||
fi
|
||||
|
||||
@@ -22,7 +22,8 @@ plugins=(... bundler)
|
||||
| `bo` | `bundle open` | Opens the source directory for a gem in your bundle |
|
||||
| `bout` | `bundle outdated` | List installed gems with newer versions available |
|
||||
| `bp` | `bundle package` | Package your needed .gem files into your application |
|
||||
| `bu` | `bundle update` | Update your gems to the latest available versions |
|
||||
| `bu` | `bundle update` | Update your gems to the latest available versions |
|
||||
| `bua` | `bundle update --all` | Update all gems to the latest available versions |
|
||||
|
||||
## Gem wrapper
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ alias bo="bundle open"
|
||||
alias bout="bundle outdated"
|
||||
alias bp="bundle package"
|
||||
alias bu="bundle update"
|
||||
alias bua="bundle update --all"
|
||||
|
||||
## Gem wrapper
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# AWS CDK Plugin
|
||||
|
||||
This plugin provides aliases and autocompletion for the [AWS Cloud Development Kit (CDK)](https://aws.amazon.com/cdk/) CLI.
|
||||
|
||||
## Usage
|
||||
|
||||
Add `cdk` to the plugins array in your `.zshrc`:
|
||||
|
||||
```zsh
|
||||
plugins=(... cdk)
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- [AWS CDK CLI](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html) installed (`npm install -g aws-cdk`)
|
||||
|
||||
## Aliases
|
||||
|
||||
| Alias | Command | Description |
|
||||
| ------------ | ----------------- | ---------------------------------- |
|
||||
| `cdkl` | `cdk list` | List all stacks in the app |
|
||||
| `cdksynth` | `cdk synth` | Synthesize CloudFormation template |
|
||||
| `cdkdiff` | `cdk diff` | Compare deployed vs local stack |
|
||||
| `cdkdeploy` | `cdk deploy` | Deploy stack to AWS |
|
||||
| `cdkdestroy` | `cdk destroy` | Destroy deployed stack |
|
||||
| `cdkboot` | `cdk bootstrap` | Bootstrap CDK environment |
|
||||
| `cdkdoc` | `cdk docs` | Open CDK documentation |
|
||||
| `cdkinit` | `cdk init` | Initialize a new CDK project |
|
||||
| `cdkwatch` | `cdk watch` | Watch for changes and auto-deploy |
|
||||
| `cdkctx` | `cdk context` | Manage cached context values |
|
||||
| `cdkack` | `cdk acknowledge` | Acknowledge a notice |
|
||||
| `cdkver` | `cdk --version` | Print CDK version |
|
||||
@@ -0,0 +1,12 @@
|
||||
#compdef cdk
|
||||
|
||||
# Originally found and adapted from: https://github.com/aws/aws-cdk/discussions/24380#discussioncomment-5158176
|
||||
|
||||
local -a reply
|
||||
local IFS
|
||||
IFS=$'\n' reply=($(COMP_CWORD="$((CURRENT-1))" \
|
||||
COMP_LINE="$BUFFER" \
|
||||
COMP_POINT="$CURSOR" \
|
||||
cdk --get-yargs-completions "${words[@]}"))
|
||||
|
||||
_describe 'values' reply
|
||||
@@ -0,0 +1,13 @@
|
||||
# Aliases
|
||||
alias cdkl='cdk list'
|
||||
alias cdksynth='cdk synth'
|
||||
alias cdkdiff='cdk diff'
|
||||
alias cdkdeploy='cdk deploy'
|
||||
alias cdkdestroy='cdk destroy'
|
||||
alias cdkboot='cdk bootstrap'
|
||||
alias cdkdoc='cdk docs'
|
||||
alias cdkinit='cdk init'
|
||||
alias cdkwatch='cdk watch'
|
||||
alias cdkctx='cdk context'
|
||||
alias cdkack='cdk acknowledge'
|
||||
alias cdkver='cdk --version'
|
||||
@@ -1,6 +1,6 @@
|
||||
# Celery
|
||||
|
||||
This plugin provides completion for [Celery](http://www.celeryproject.org/).
|
||||
This plugin provides completion for [Celery](https://docs.celeryq.dev/en/stable/).
|
||||
|
||||
To use it add celery to the plugins array in your zshrc file.
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# codex Plugin
|
||||
|
||||
## Introduction
|
||||
|
||||
This `codex` plugin sets up completion for [codex](https://github.com/openai/codex).
|
||||
|
||||
To use it, add `codex` to the plugins array of your zshrc file:
|
||||
|
||||
```bash
|
||||
plugins=(... codex)
|
||||
```
|
||||
@@ -0,0 +1,14 @@
|
||||
# COMPLETION FUNCTION
|
||||
if (( ! $+commands[codex] )); then
|
||||
return
|
||||
fi
|
||||
|
||||
# If the completion file doesn't exist yet, we need to autoload it and
|
||||
# bind it to `codex`. Otherwise, compinit will have already done that.
|
||||
if [[ ! -f "$ZSH_CACHE_DIR/completions/_codex" ]]; then
|
||||
typeset -g -A _comps
|
||||
autoload -Uz _codex
|
||||
_comps[codex]=_codex
|
||||
fi
|
||||
|
||||
codex completion zsh < /dev/null 2> /dev/null >| "$ZSH_CACHE_DIR/completions/_codex" &|
|
||||
@@ -32,5 +32,6 @@ It works out of the box with the command-not-found packages for:
|
||||
- [SUSE](https://www.unix.com/man-page/suse/1/command-not-found/)
|
||||
- [Gentoo](https://github.com/AndrewAmmerlaan/command-not-found-gentoo/tree/main)
|
||||
- [Void Linux](https://codeberg.org/classabbyamp/xbps-command-not-found)
|
||||
- [Alpine Linux](https://pkgs.alpinelinux.org/package/edge/main/x86_64/command-not-found)
|
||||
|
||||
You can add support for other platforms by submitting a Pull Request.
|
||||
|
||||
@@ -74,3 +74,10 @@ if [[ -x /usr/bin/command-not-found ]]; then
|
||||
/usr/bin/command-not-found "$1"
|
||||
}
|
||||
fi
|
||||
|
||||
# Alpine: https://gitlab.alpinelinux.org/alpine/aports/-/blob/master/main/command-not-found/APKBUILD
|
||||
if [[ -x /usr/libexec/command-not-found ]]; then
|
||||
command_not_found_handler() {
|
||||
/usr/libexec/command-not-found "$1"
|
||||
}
|
||||
fi
|
||||
|
||||
@@ -78,7 +78,7 @@ if is-at-least 4.2.0; then
|
||||
alias -s chm=xchm
|
||||
alias -s djvu=djview
|
||||
|
||||
#list whats inside packed file
|
||||
#list what's inside packed file
|
||||
alias -s zip="unzip -l"
|
||||
alias -s rar="unrar l"
|
||||
alias -s tar="tar tf"
|
||||
|
||||
+14
-14
@@ -4,17 +4,17 @@ This plugin sets up completion and aliases for [Deno](https://deno.land).
|
||||
|
||||
## Aliases
|
||||
|
||||
| Alias | Full command |
|
||||
| ----- | ------------------- |
|
||||
| db | deno bundle |
|
||||
| dc | deno compile |
|
||||
| dca | deno cache |
|
||||
| dfmt | deno fmt |
|
||||
| dh | deno help |
|
||||
| dli | deno lint |
|
||||
| drn | deno run |
|
||||
| drA | deno run -A |
|
||||
| drw | deno run --watch |
|
||||
| dru | deno run --unstable |
|
||||
| dts | deno test |
|
||||
| dup | deno upgrade |
|
||||
| Alias | Full command |
|
||||
| ----- | ---------------- |
|
||||
| dc | deno compile |
|
||||
| dca | deno cache |
|
||||
| dck | deno check |
|
||||
| dfmt | deno fmt |
|
||||
| dh | deno help |
|
||||
| dli | deno lint |
|
||||
| drn | deno run |
|
||||
| drA | deno run -A |
|
||||
| drw | deno run --watch |
|
||||
| dsv | deno serve |
|
||||
| dts | deno test |
|
||||
| dup | deno upgrade |
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# ALIASES
|
||||
alias db='deno bundle'
|
||||
alias dc='deno compile'
|
||||
alias dca='deno cache'
|
||||
alias dck='deno check'
|
||||
alias dfmt='deno fmt'
|
||||
alias dh='deno help'
|
||||
alias dli='deno lint'
|
||||
alias drn='deno run'
|
||||
alias drA='deno run -A'
|
||||
alias drw='deno run --watch'
|
||||
alias dru='deno run --unstable'
|
||||
alias dsv='deno serve'
|
||||
alias dts='deno test'
|
||||
alias dup='deno upgrade'
|
||||
|
||||
|
||||
+16
-15
@@ -15,18 +15,19 @@ of `dnf5` and uses it as drop-in alternative to the slower `dnf`.
|
||||
|
||||
## Aliases
|
||||
|
||||
| Alias | Command | Description |
|
||||
|-------|-------------------------|--------------------------|
|
||||
| dnfl | `dnf list` | List packages |
|
||||
| dnfli | `dnf list installed` | List installed packages |
|
||||
| dnfgl | `dnf grouplist` | List package groups |
|
||||
| dnfmc | `dnf makecache` | Generate metadata cache |
|
||||
| dnfp | `dnf info` | Show package information |
|
||||
| dnfs | `dnf search` | Search package |
|
||||
| **Use `sudo`** |
|
||||
| dnfu | `sudo dnf upgrade` | Upgrade package |
|
||||
| dnfi | `sudo dnf install` | Install package |
|
||||
| dnfgi | `sudo dnf groupinstall` | Install package group |
|
||||
| dnfr | `sudo dnf remove` | Remove package |
|
||||
| dnfgr | `sudo dnf groupremove` | Remove package group |
|
||||
| dnfc | `sudo dnf clean all` | Clean cache |
|
||||
| Alias | Command | Description |
|
||||
|-------|-------------------------------|------------------------------------------|
|
||||
| dnfl | `dnf list` | List packages |
|
||||
| dnfli | `dnf list --installed` | List installed packages |
|
||||
| dnfgl | `dnf grouplist` | List package groups |
|
||||
| dnfmc | `dnf makecache` | Generate metadata cache |
|
||||
| dnfp | `dnf info` | Show package information |
|
||||
| dnfs | `dnf search` | Search package |
|
||||
| **Use `sudo`** |
|
||||
| dnfu | `sudo dnf upgrade` | Upgrade package |
|
||||
| dnfur | `sudo dnf upgrade --refresh` | Upgrade package (force metadata refresh) |
|
||||
| dnfi | `sudo dnf install` | Install package |
|
||||
| dnfgi | `sudo dnf groupinstall` | Install package group |
|
||||
| dnfr | `sudo dnf remove` | Remove package |
|
||||
| dnfgr | `sudo dnf groupremove` | Remove package group |
|
||||
| dnfc | `sudo dnf clean all` | Clean cache |
|
||||
|
||||
@@ -5,12 +5,13 @@ local dnfprog="dnf"
|
||||
command -v dnf5 > /dev/null && dnfprog=dnf5
|
||||
|
||||
alias dnfl="${dnfprog} list" # List packages
|
||||
alias dnfli="${dnfprog} list installed" # List installed packages
|
||||
alias dnfli="${dnfprog} list --installed" # List installed packages
|
||||
alias dnfmc="${dnfprog} makecache" # Generate metadata cache
|
||||
alias dnfp="${dnfprog} info" # Show package information
|
||||
alias dnfs="${dnfprog} search" # Search package
|
||||
|
||||
alias dnfu="sudo ${dnfprog} upgrade" # Upgrade package
|
||||
alias dnfur="sudo ${dnfprog} upgrade --refresh" # Upgrade package and refresh repos
|
||||
alias dnfi="sudo ${dnfprog} install" # Install package
|
||||
alias dnfr="sudo ${dnfprog} remove" # Remove package
|
||||
alias dnfc="sudo ${dnfprog} clean all" # Clean cache
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
tap: false
|
||||
directories:
|
||||
tests: tests
|
||||
output: tests/_output
|
||||
support: tests/_support
|
||||
time_limit: 0
|
||||
fail_fast: false
|
||||
allow_risky: false
|
||||
verbose: false
|
||||
@@ -34,6 +34,25 @@ PORT=3001
|
||||
|
||||
You can even mix both formats, although it's probably a bad idea.
|
||||
|
||||
Multi-line values are supported using quoted strings:
|
||||
|
||||
```sh
|
||||
PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEA...
|
||||
-----END RSA PRIVATE KEY-----"
|
||||
```
|
||||
|
||||
Variables defined earlier in the file can be referenced by later entries:
|
||||
|
||||
```sh
|
||||
BASE_URL=https://example.com
|
||||
API_URL=$BASE_URL/api
|
||||
ASSETS_URL=${BASE_URL}/assets
|
||||
```
|
||||
|
||||
Note: only variables defined within the same `.env` file are expanded this way —
|
||||
shell environment variables that already exist are **not** substituted.
|
||||
|
||||
## Settings
|
||||
|
||||
### ZSH_DOTENV_FILE
|
||||
@@ -86,13 +105,37 @@ mount `.env` files as named pipes to inject secrets on-the-fly without writing t
|
||||
|
||||
No additional configuration is required — the plugin automatically detects and sources named pipes.
|
||||
|
||||
## Tests
|
||||
|
||||
The tests use [zunit](https://github.com/zunit-zsh/zunit). Install it per its [documentation](https://github.com/zunit-zsh/zunit#installation), then run:
|
||||
|
||||
```sh
|
||||
cd plugins/dotenv && zunit
|
||||
```
|
||||
|
||||
> [NOTE!]
|
||||
> zunit also requires installing [Revolver](https://github.com/molovo/revolver).
|
||||
|
||||
## Version Control
|
||||
|
||||
**It's strongly recommended to add `.env` file to `.gitignore`**, because usually it contains sensitive information such as your credentials, secret keys, passwords etc. You don't want to commit this file, it's supposed to be local only.
|
||||
|
||||
## Disclaimer
|
||||
## Security
|
||||
|
||||
This plugin only sources the `.env` file. Nothing less, nothing more. It doesn't do any checks. It's designed to be the fastest and simplest option. You're responsible for the `.env` file content. You can put some code (or weird symbols) there, but do it on your own risk. `dotenv` is the basic tool, yet it does the job.
|
||||
The plugin applies several best-effort safeguards when loading a `.env` file:
|
||||
|
||||
- **Size limit** — files larger than 10 MiB are rejected to prevent DoS.
|
||||
- **Syntax check** — the file is validated with `zsh -fn` before any variables are set.
|
||||
- **No command substitution** — entries containing `$(...)` or backtick constructs are skipped.
|
||||
- **Forbidden variables** — the following variables are never overwritten, regardless of what the
|
||||
`.env` file contains: `NODE_OPTIONS`, `BASH_ENV`, `ENV`, `ZDOTDIR`, `ZSH`, `LD_PRELOAD`,
|
||||
`LD_LIBRARY_PATH`, `DYLD_INSERT_LIBRARIES`, `GIT_CONFIG_GLOBAL`, `GIT_DIR`, `GIT_EDITOR`,
|
||||
`GIT_EXTERNAL_DIFF`, `GIT_EXEC_PATH`, `GIT_PAGER`, `GIT_SSH`, `GIT_SSH_COMMAND`,
|
||||
`GIT_SSL_NO_VERIFY`, `GIT_TEMPLATE_DIR`, `VISUAL`, `PAGER`, `EDITOR`, and all zsh special
|
||||
parameters.
|
||||
|
||||
These measures are **best-effort** — you are still responsible for the content of your `.env`
|
||||
file. Do not use this plugin as a security boundary.
|
||||
|
||||
If you need more advanced and feature-rich ENV management, check out these awesome projects:
|
||||
|
||||
|
||||
@@ -7,9 +7,271 @@
|
||||
: ${ZSH_DOTENV_ALLOWED_LIST:="${ZSH_CACHE_DIR:-$ZSH/cache}/dotenv-allowed.list"}
|
||||
: ${ZSH_DOTENV_DISALLOWED_LIST:="${ZSH_CACHE_DIR:-$ZSH/cache}/dotenv-disallowed.list"}
|
||||
|
||||
|
||||
## Functions
|
||||
|
||||
_parse_dotenv_content() {
|
||||
setopt localoptions extendedglob
|
||||
|
||||
local content="$1"
|
||||
local mode="${2:-export}"
|
||||
|
||||
# Validate mode argument
|
||||
case "$mode" in
|
||||
export|test) ;;
|
||||
*)
|
||||
echo "parse_dotenv: invalid mode '$mode' (use 'export' or 'test')" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
local node line key value
|
||||
local raw_value expanded prefix remainder var_name escaped_dollar_placeholder
|
||||
local sq dq uq safe
|
||||
local -A parsed_vars
|
||||
local -a nodes lines
|
||||
|
||||
# Parse into command lines separated by `;`, with built-in support for multi-line commands.
|
||||
# (Z:C:) ignores comments and preserves quotes and escapes.
|
||||
#
|
||||
# All logical commands are separated by literal ';' elements, which allows us to reconstruct logical lines
|
||||
# by joining all elements between ';'.
|
||||
#
|
||||
# Example input:
|
||||
# VAR1=value1; VAR2=value2
|
||||
# VAR3="multi
|
||||
# line value"
|
||||
# Result:
|
||||
# typeset -a nodes=( 'VAR1=value1' ';' 'VAR2=value2' ';' $'VAR3="multi\nline value"' )
|
||||
# typeset -a lines=( 'VAR1=value1' 'VAR2=value2' $'VAR3="multi\nline value"' )
|
||||
#
|
||||
nodes=("${(@Z:C:)content}" ";") # last ';' ensures we add the final command
|
||||
for node in "${nodes[@]}"; do
|
||||
if [[ "$node" == ";" ]]; then
|
||||
if [[ -n "$line" ]]; then
|
||||
lines+=("$line")
|
||||
line=""
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
|
||||
[[ -z "$line" ]] || line+=" "
|
||||
line+="$node"
|
||||
done
|
||||
|
||||
local -a forbidden_vars=(
|
||||
NODE_OPTIONS
|
||||
BASH_ENV
|
||||
ENV
|
||||
ZDOTDIR
|
||||
ZSH
|
||||
LD_PRELOAD
|
||||
LD_LIBRARY_PATH
|
||||
DYLD_INSERT_LIBRARIES
|
||||
GIT_CONFIG_GLOBAL
|
||||
GIT_DIR
|
||||
GIT_EDITOR
|
||||
GIT_EXTERNAL_DIFF
|
||||
GIT_EXEC_PATH
|
||||
GIT_PAGER
|
||||
GIT_SSH
|
||||
GIT_SSH_COMMAND
|
||||
GIT_SSL_NO_VERIFY
|
||||
GIT_TEMPLATE_DIR
|
||||
VISUAL
|
||||
PAGER
|
||||
EDITOR
|
||||
${(k)parameters[(R)*export*special]}
|
||||
)
|
||||
local forbidden="${(j:|:)forbidden_vars}"
|
||||
|
||||
|
||||
# Each line contains a single command line, we need to parse valid KEY=VALUE pairs
|
||||
for line in "${lines[@]}"; do
|
||||
# Strip leading 'export ' keyword
|
||||
line="${line#export[ ]}"
|
||||
|
||||
# Match KEY=VALUE pattern
|
||||
# "A name may be any sequence of alphanumeric characters and underscores"
|
||||
# https://zsh.sourceforge.io/Doc/Release/Parameters.html#Parameters
|
||||
if [[ ! "$line" =~ ^([a-zA-Z_][a-zA-Z0-9_]*)=(.*)$ ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
key="${match[1]}"
|
||||
value="${match[2]}"
|
||||
raw_value="$value"
|
||||
|
||||
# Filter out variables to be ignored for security reasons (best effort)
|
||||
if [[ "$key" == (${~forbidden}) ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Use tokenization to split value with native shell parsing (handles quotes and escapes)
|
||||
# Ignore any values that parse to multiple words, e.g. `BASE_URL=/ echo command run`
|
||||
local -a words
|
||||
words=("${(@z)value}")
|
||||
if [[ ${#words} -ne 1 ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
## START: FILTER COMMAND EXPANSION
|
||||
#
|
||||
# Filter lines with command expansion not in safe contexts
|
||||
#
|
||||
# READER'S NOTE: this is actually a "best effort" check (works in tests), but
|
||||
# only to prevent setting variables with command substitution. The actual effect
|
||||
# of setting them would not be a vulnerability, because we use `typeset name=value`
|
||||
# and value is a quoted string parsed by zsh itself with `${(Z:C:)content}`.
|
||||
#
|
||||
# What does this mean? If we were to remove this filter block, this is what would happen:
|
||||
#
|
||||
# Input: DANGEROUS=$(echo this is a command)
|
||||
# Output: DANGEROUS='$(echo this is a command)' (literal string, no command execution)
|
||||
#
|
||||
# Check for potential command substitution outside of safe contexts
|
||||
# - single-quoted strings: command substitution is literal there
|
||||
sq="'[^']#'"
|
||||
# - double-quoted strings, but NOT unescaped ` or $(
|
||||
dq='"([^"$`\\]|\\.|\\$[^(\`])#"'
|
||||
# - unquoted text, but NOT unescaped ` or $(
|
||||
uq='([^$`'"'"'"\\]|\\.|\\$[^(\`])#'
|
||||
safe="(${sq}|${dq}|${uq})#"
|
||||
# Remove the longest safe prefix; what remains starts at first unsafe construct
|
||||
remainder="${value##${~safe}}"
|
||||
|
||||
if [[ "$remainder" == *'$('* || "$remainder" == *'`'* ]]; then
|
||||
continue
|
||||
fi
|
||||
## END: FILTER COMMAND EXPANSION
|
||||
|
||||
# Single-quoted values are fully literal and must not participate in expansion.
|
||||
if [[ "$raw_value" == \'*\' ]]; then
|
||||
value="${(Q)value}"
|
||||
parsed_vars[$key]="$value"
|
||||
if [[ "$mode" == "export" ]]; then
|
||||
typeset -x "$key"="$value"
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
|
||||
# Preserve escaped dollars so they remain literal after unquoting.
|
||||
escaped_dollar_placeholder=$'\001DOTENV_ESCAPED_DOLLAR\001'
|
||||
value="${value//\\\$/$escaped_dollar_placeholder}"
|
||||
|
||||
# Unquote the value to handle special characters and multiline values.
|
||||
value="${(Q)value}"
|
||||
|
||||
# Expand previously parsed in-file variables without partial name matches.
|
||||
expanded=""
|
||||
prefix=""
|
||||
remainder="$value"
|
||||
var_name=""
|
||||
while [[ "$remainder" == *'$'* ]]; do
|
||||
prefix="${remainder%%\$*}"
|
||||
expanded+="$prefix"
|
||||
remainder="${remainder#$prefix}"
|
||||
|
||||
if [[ "$remainder" =~ '^\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}(.*)$' ]]; then
|
||||
var_name="${match[1]}"
|
||||
remainder="${match[2]}"
|
||||
elif [[ "$remainder" =~ '^\$([a-zA-Z_][a-zA-Z0-9_]*)(.*)$' ]]; then
|
||||
var_name="${match[1]}"
|
||||
remainder="${match[2]}"
|
||||
else
|
||||
expanded+='$'
|
||||
remainder="${remainder#?}"
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ -v "parsed_vars[$var_name]" ]]; then
|
||||
expanded+="${parsed_vars[$var_name]}"
|
||||
fi
|
||||
done
|
||||
value="${expanded}${remainder}"
|
||||
value="${value//$escaped_dollar_placeholder/\$}"
|
||||
|
||||
# Store in parsed vars (for in-file expansion)
|
||||
parsed_vars[$key]="$value"
|
||||
|
||||
# Normal mode: export the variable
|
||||
if [[ "$mode" == "export" ]]; then
|
||||
typeset -x "$key"="$value"
|
||||
fi
|
||||
done
|
||||
|
||||
# In test mode, set DOTENV_TEST_VARS
|
||||
typeset -gA DOTENV_TEST_VARS
|
||||
DOTENV_TEST_VARS=("${(@kv)parsed_vars}")
|
||||
}
|
||||
|
||||
parse_dotenv() {
|
||||
local filename="$1"
|
||||
local mode="${2:-export}"
|
||||
local content
|
||||
|
||||
# Fail if file is too large to avoid DoS
|
||||
zmodload -F zsh/stat b:zstat
|
||||
local -i file_size max_size=10485760 # 10MiB
|
||||
if ! file_size=$(zstat +size "$filename" 2>/dev/null); then
|
||||
echo "dotenv: unable to determine size of file '$filename'" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if (( file_size > max_size )); then
|
||||
echo "dotenv: file '$filename' is too large to parse (size: $file_size bytes)" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
content="$(<"$filename")" || return 1
|
||||
_parse_dotenv_content "$content" "$mode"
|
||||
}
|
||||
|
||||
_dotenv_read_limited() {
|
||||
local filename="$1"
|
||||
local chunk content=""
|
||||
local -i max_size=10485760 total=0 read_size=0 fd read_status
|
||||
|
||||
zmodload zsh/system || return 1
|
||||
exec {fd}<"$filename" || return 1
|
||||
|
||||
while true; do
|
||||
sysread -i $fd -s 65536 -c read_size chunk
|
||||
read_status=$?
|
||||
|
||||
if (( read_status == 5 )); then
|
||||
break
|
||||
elif (( read_status != 0 )); then
|
||||
exec {fd}<&-
|
||||
return 1
|
||||
fi
|
||||
|
||||
(( total += read_size ))
|
||||
if (( total > max_size )); then
|
||||
exec {fd}<&-
|
||||
echo "dotenv: file '$filename' is too large to parse (size: more than $max_size bytes)" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
content+="$chunk"
|
||||
done
|
||||
|
||||
exec {fd}<&-
|
||||
REPLY="$content"
|
||||
}
|
||||
|
||||
_dotenv_check_syntax() {
|
||||
local filename="$1"
|
||||
|
||||
if (( $# == 2 )); then
|
||||
printf '%s' "$2" | zsh -fn /dev/stdin
|
||||
else
|
||||
zsh -fn -- "$filename"
|
||||
fi || {
|
||||
echo "dotenv: error when sourcing '$filename' file" >&2
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
source_env() {
|
||||
if [[ ! -f "$ZSH_DOTENV_FILE" ]] && [[ ! -p "$ZSH_DOTENV_FILE" ]]; then
|
||||
return
|
||||
@@ -37,28 +299,35 @@ source_env() {
|
||||
[[ $column -eq 1 ]] || echo
|
||||
|
||||
# print same-line prompt and output newline character if necessary
|
||||
echo -n "dotenv: found '$ZSH_DOTENV_FILE' file. Source it? ([Y]es/[n]o/[a]lways/n[e]ver) "
|
||||
echo -n "dotenv: found '$ZSH_DOTENV_FILE' file. Source it? ([y]es/[N]o/[a]lways/n[e]ver) "
|
||||
read -k 1 confirmation
|
||||
[[ "$confirmation" = $'\n' ]] || echo
|
||||
|
||||
# check input
|
||||
case "$confirmation" in
|
||||
[nN]) return ;;
|
||||
[yY]) ;;
|
||||
[aA]) echo "$dirpath" >> "$ZSH_DOTENV_ALLOWED_LIST" ;;
|
||||
[eE]) echo "$dirpath" >> "$ZSH_DOTENV_DISALLOWED_LIST"; return ;;
|
||||
*) ;; # interpret anything else as a yes
|
||||
*) return ;; # interpret anything else as a no
|
||||
esac
|
||||
fi
|
||||
fi
|
||||
|
||||
# test .env syntax
|
||||
zsh -fn $ZSH_DOTENV_FILE || {
|
||||
echo "dotenv: error when sourcing '$ZSH_DOTENV_FILE' file" >&2
|
||||
return 1
|
||||
}
|
||||
local content
|
||||
if [[ -p "$ZSH_DOTENV_FILE" ]]; then
|
||||
_dotenv_read_limited "$ZSH_DOTENV_FILE" || return 1
|
||||
content="$REPLY"
|
||||
_dotenv_check_syntax "$ZSH_DOTENV_FILE" "$content" || return 1
|
||||
|
||||
setopt localoptions allexport
|
||||
_parse_dotenv_content "$content"
|
||||
return
|
||||
fi
|
||||
|
||||
_dotenv_check_syntax "$ZSH_DOTENV_FILE" || return 1
|
||||
|
||||
setopt localoptions allexport
|
||||
source $ZSH_DOTENV_FILE
|
||||
parse_dotenv "$ZSH_DOTENV_FILE"
|
||||
}
|
||||
|
||||
autoload -U add-zsh-hook
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env zsh
|
||||
# Bootstrap script for dotenv plugin tests
|
||||
# This is sourced before any tests run and provides shared utilities
|
||||
|
||||
# Load the dotenv plugin
|
||||
source "$PWD/dotenv.plugin.zsh"
|
||||
ZSH_DOTENV_PROMPT=false
|
||||
ZSH_DOTENV_FILE=/dev/null
|
||||
|
||||
# Helper: Parse dotenv file in test mode
|
||||
_parse_dotenv_test() {
|
||||
parse_dotenv "$1" "test"
|
||||
}
|
||||
|
||||
# Helper: Parse dotenv file in export mode
|
||||
_parse_dotenv_export() {
|
||||
unset "${(k)parameters[(R)*export*]}" 2>/dev/null || true
|
||||
|
||||
parse_dotenv "$1" "test"
|
||||
|
||||
for key in "${(k)DOTENV_TEST_VARS}"; do
|
||||
typeset -x "$key"="${DOTENV_TEST_VARS[$key]}"
|
||||
done
|
||||
}
|
||||
|
||||
# Helper: Run parse_dotenv suppressing stderr
|
||||
_parse_dotenv_quiet() {
|
||||
parse_dotenv "$@" 2>/dev/null
|
||||
}
|
||||
|
||||
# Helper: Create a temporary test fixture
|
||||
_create_temp_fixture() {
|
||||
local fixture
|
||||
fixture==(:) # Create temp file
|
||||
echo "$fixture"
|
||||
}
|
||||
|
||||
_write_temp_fixture() {
|
||||
local fixture="$1"
|
||||
> "$fixture"
|
||||
}
|
||||
|
||||
|
||||
# Helper: Source file with allexport and capture variables
|
||||
# Usage: _source_with_allexport "file.env"
|
||||
# Result is in DOTENV_SOURCE_VARS associative array
|
||||
_source_with_allexport() {
|
||||
local filename="$1"
|
||||
|
||||
# Source with allexport in a subshell with no exported variables
|
||||
|
||||
# The return and capture of the exported variables is a bit of a pain:
|
||||
# 1. We first store the key=value pairs in $vars associative array, which is
|
||||
# defined before allexport is set to avoid appearing in results.
|
||||
# 2. Afterwards, we join all keys and values of the associative with null delimiters. With
|
||||
# "$(@kv)vars}" we get keys and values with quotes, to retain empty values. With (pj:\0:)
|
||||
# we join them with nulls.
|
||||
# 3. The caller reads this output with "${(@0)}" to split by nulls and quoting to retain
|
||||
# empty values, and then uses it to populate an associative array.
|
||||
# Don't try to understand this or change it unless you have to. Debugging is a nightmare.
|
||||
typeset -gA DOTENV_SOURCE_VARS
|
||||
DOTENV_SOURCE_VARS=("${(@0)"$(
|
||||
local -A vars
|
||||
|
||||
# Clear all exports first
|
||||
zmodload zsh/parameter
|
||||
unset ${(k)parameters[(R)*export*]} 2>/dev/null || true
|
||||
|
||||
# Source file with allexport
|
||||
setopt localoptions allexport
|
||||
source "$filename"
|
||||
|
||||
# Set all exported variables into an associative array
|
||||
for key in ${(k)parameters[(R)*export*]}; do
|
||||
vars[$key]="${(P)key}"
|
||||
done
|
||||
|
||||
print -rn -- "${(@kvpj:\0:)vars}"
|
||||
)"}")
|
||||
}
|
||||
|
||||
|
||||
## ZUnit assertion helpers
|
||||
|
||||
_zunit_assert_function_exists() {
|
||||
[[ "${+functions[$1]}" -eq 1 ]] && return 0
|
||||
echo "Function '$1' does not exist"
|
||||
exit 1
|
||||
}
|
||||
|
||||
_zunit_assert_var_same_as() {
|
||||
local tvalue=${${:-${(Pt)1%-*}}:-unset} tcomp=${${:-${(Pt)2%-*}}:-unset}
|
||||
if [[ $tvalue != $tcomp ]]; then
|
||||
echo "Type mismatch: '$1' ($tvalue) and '$2' ($tcomp)"
|
||||
exit 78
|
||||
fi
|
||||
|
||||
# Special case for associative arrays
|
||||
if [[ ${(Pt)1} == "association" ]]; then
|
||||
local -A value=("${(P@kv)1}") comparison=("${(P@kv)2}")
|
||||
local -aU keys=("${(@k)value}" "${(@k)comparison}")
|
||||
|
||||
local ret=0 key
|
||||
for key in "${keys[@]}"; do
|
||||
# Key match checks
|
||||
if [[ -v "value[$key]" && ! -v "comparison[$key]" ]]; then
|
||||
echo "'$1[$key]' is set (value='${value[$key]}')"
|
||||
ret=1
|
||||
elif [[ ! -v "value[$key]" && -v "comparison[$key]" ]]; then
|
||||
echo "'$1[$key]' is not set (expected='${comparison[$key]}')"
|
||||
ret=1
|
||||
# Value match checks
|
||||
elif [[ "${value[$key]}" != "${comparison[$key]}" ]]; then
|
||||
echo "'$1[$key]' value mismatch: '${value[$key]}' is not the same as '${comparison[$key]}'"
|
||||
ret=1
|
||||
fi
|
||||
done
|
||||
|
||||
exit $ret
|
||||
fi
|
||||
|
||||
# Generic case
|
||||
local value="${(P)1}" comparison="${(P)2}"
|
||||
[[ "$value" != "$comparison" ]] || exit 0
|
||||
echo "'$1' value mismatch: '$value' is not the same as '$comparison'"
|
||||
exit 1
|
||||
}
|
||||
|
||||
_zunit_assert_var_is_set() {
|
||||
[[ -v "$1" ]] && return 0
|
||||
echo "Variable '$1' is not set"
|
||||
exit 1
|
||||
}
|
||||
|
||||
_zunit_assert_var_is_not_set() {
|
||||
[[ ! -v "$1" ]] && return 0
|
||||
echo "Variable '$1' is set"
|
||||
exit 1
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
# Consolidated dotenv test fixture from dotenv test suite
|
||||
# Source: https://github.com/motdotla/dotenv/tree/master/tests
|
||||
#
|
||||
# Copyright (c) 2015, Scott Motte
|
||||
# All rights reserved.
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
|
||||
# * Redistributions of source code must retain the above copyright notice, this
|
||||
# list of conditions and the following disclaimer.
|
||||
|
||||
# * Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
# Basic assignments
|
||||
BASIC=basic
|
||||
|
||||
# previous line intentionally left blank
|
||||
AFTER_LINE=after_line
|
||||
|
||||
# Empty values
|
||||
EMPTY=
|
||||
EMPTY_SINGLE_QUOTES=''
|
||||
EMPTY_DOUBLE_QUOTES=""
|
||||
|
||||
# Single quotes (literal, no expansion)
|
||||
SINGLE_QUOTES='single_quotes'
|
||||
SINGLE_QUOTES_SPACED=' single quotes '
|
||||
DONT_EXPAND_SQUOTED='dontexpand\nnewlines'
|
||||
|
||||
# Double quotes (with escapes)
|
||||
DOUBLE_QUOTES="double_quotes"
|
||||
DOUBLE_QUOTES_SPACED=" double quotes "
|
||||
EXPAND_NEWLINES="expand\nnew\nlines"
|
||||
|
||||
# Unquoted (no escape expansion)
|
||||
DONT_EXPAND_UNQUOTED=dontexpand\nnewlines
|
||||
|
||||
# Quotes inside quotes
|
||||
DOUBLE_QUOTES_INSIDE_SINGLE='double "quotes" work inside single quotes'
|
||||
SINGLE_QUOTES_INSIDE_DOUBLE="single 'quotes' work inside double quotes"
|
||||
|
||||
# Comments
|
||||
# COMMENTS=work
|
||||
INLINE_COMMENTS_SINGLE_QUOTES='inline comments outside of #singlequotes' # work
|
||||
INLINE_COMMENTS_DOUBLE_QUOTES="inline comments outside of #doublequotes" # work
|
||||
INLINE_COMMENTS_UNQUOTED=value # work
|
||||
|
||||
# Special characters
|
||||
EQUAL_SIGNS=equals==
|
||||
RETAIN_INNER_QUOTES_AS_STRING='{"foo": "bar"}'
|
||||
USEREMAIL=therealnerdybeast@example.tld
|
||||
|
||||
# Multiline values with double quotes
|
||||
MULTI_DOUBLE_QUOTED="THIS
|
||||
IS
|
||||
A
|
||||
MULTILINE
|
||||
STRING"
|
||||
|
||||
# Multiline values with single quotes
|
||||
MULTI_SINGLE_QUOTED='THIS
|
||||
IS
|
||||
A
|
||||
MULTILINE
|
||||
STRING'
|
||||
|
||||
# Multiline PEM certificate
|
||||
MULTI_PEM_DOUBLE_QUOTED="-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnNl1tL3QjKp3DZWM0T3u
|
||||
LgGJQwu9WqyzHKZ6WIA5T+7zPjO1L8l3S8k8YzBrfH4mqWOD1GBI8Yjq2L1ac3Y/
|
||||
bTdfHN8CmQr2iDJC0C6zY8YV93oZB3x0zC/LPbRYpF8f6OqX1lZj5vo2zJZy4fI/
|
||||
kKcI5jHYc8VJq+KCuRZrvn+3V+KuL9tF9v8ZgjF2PZbU+LsCy5Yqg1M8f5Jp5f6V
|
||||
u4QuUoobAgMBAAE=
|
||||
-----END PUBLIC KEY-----"
|
||||
@@ -0,0 +1,23 @@
|
||||
# Export syntax
|
||||
export EXPORTED_VAR=exported_value
|
||||
export EXPORTED_EMPTY=
|
||||
|
||||
# Variable expansion (in-file forward references)
|
||||
BASE_URL=https://api.example.com
|
||||
API_ENDPOINT="${BASE_URL}/v1"
|
||||
FULL_ENDPOINT=$BASE_URL/v2/users
|
||||
COMBINED="${BASE_URL}_suffix"
|
||||
|
||||
# Testing multiline quoting edge cases
|
||||
MULTILINE_UNQUOTED=This\ is\ a\ \
|
||||
multiline\ value\ that\ should\ be\ treated\ as\ a\ single\ line\ with\ a\ literal\ backslash\ and\ newline
|
||||
MULTILINE_DOUBLE_QUOTED="This is a \
|
||||
multiline value that should be treated as a single line with an actual newline character"
|
||||
MULTILINE_SINGLE_QUOTED='This is a \
|
||||
multiline value that should be treated as a single line with a literal backslash and newline'
|
||||
MULTILINE_MIXED_QUOTES="This is a \
|
||||
multiline value that should be treated as a single line with an actual newline character and a literal backslash \"and 'single quotes' inside"
|
||||
|
||||
# Test for regressions
|
||||
DATABASE_URL="postgres://user:pass@host/db;sslmode=require"
|
||||
VAR_WITH_SEMICOLONS="value ; with ; semicolons"
|
||||
@@ -0,0 +1,398 @@
|
||||
#!/usr/bin/env zunit
|
||||
|
||||
|
||||
@setup {
|
||||
typeset -g fixture="$(_create_temp_fixture)"
|
||||
typeset -gA expected_vars=()
|
||||
}
|
||||
|
||||
@teardown {
|
||||
[[ -f "$fixture" ]] && command rm -f "$fixture"
|
||||
unset DOTENV_TEST_VARS DOTENV_SOURCE_VARS 2>/dev/null
|
||||
}
|
||||
|
||||
@test 'dotenv plugin loads successfully' {
|
||||
assert "parse_dotenv" function_exists
|
||||
assert "source_env" function_exists
|
||||
}
|
||||
|
||||
@test 'parse returns error for unsupported mode' {
|
||||
run _parse_dotenv_quiet "/dev/null" "export"
|
||||
assert $state equals 0
|
||||
|
||||
run _parse_dotenv_quiet "/dev/null" "test"
|
||||
assert $state equals 0
|
||||
|
||||
run _parse_dotenv_quiet "/dev/null" "invalid_mode"
|
||||
assert $state equals 1
|
||||
}
|
||||
|
||||
@test 'parse returns error for oversized file (> 10MiB)' {
|
||||
command truncate -s 11M "$fixture" 2>/dev/null
|
||||
|
||||
run _parse_dotenv_quiet "$fixture" "test"
|
||||
assert $state equals 1
|
||||
}
|
||||
|
||||
@test 'parse returns error for non-existent file' {
|
||||
run _parse_dotenv_quiet "/nonexistent/path/.env" "test"
|
||||
assert $state equals 1
|
||||
}
|
||||
|
||||
@test 'source_env loads named pipes without blocking' {
|
||||
local tmpdir fifo output result
|
||||
local child_pid writer_pid killer_pid child_rc
|
||||
|
||||
tmpdir="$(mktemp -d "${TMPDIR:-/tmp}/dotenv.XXXXXX")"
|
||||
fifo="$tmpdir/.env"
|
||||
output="$tmpdir/output"
|
||||
command mkfifo "$fifo"
|
||||
|
||||
(
|
||||
print -r -- 'TOKEN=secret' > "$fifo"
|
||||
) &
|
||||
writer_pid=$!
|
||||
|
||||
(
|
||||
ZSH_DOTENV_PROMPT=false
|
||||
ZSH_DOTENV_FILE="$fifo"
|
||||
source_env
|
||||
print -r -- "${TOKEN-<unset>}" > "$output"
|
||||
) &
|
||||
child_pid=$!
|
||||
|
||||
(
|
||||
sleep 2
|
||||
kill -0 $child_pid 2>/dev/null || exit 0
|
||||
kill $child_pid 2>/dev/null || exit 0
|
||||
) &
|
||||
killer_pid=$!
|
||||
|
||||
wait $child_pid
|
||||
child_rc=$?
|
||||
|
||||
kill $killer_pid 2>/dev/null || true
|
||||
kill $writer_pid 2>/dev/null || true
|
||||
wait $writer_pid 2>/dev/null || true
|
||||
|
||||
[[ -f "$output" ]] && result="$(<"$output")"
|
||||
command rm -rf "$tmpdir"
|
||||
|
||||
assert $child_rc equals 0
|
||||
assert "$result" equals 'secret'
|
||||
}
|
||||
|
||||
@test 'source_env rejects oversized named pipes' {
|
||||
run zsh -fc '
|
||||
source ./dotenv.plugin.zsh
|
||||
|
||||
tmpdir="$(mktemp -d "${TMPDIR:-/tmp}/dotenv.XXXXXX")" || exit 1
|
||||
fifo="$tmpdir/.env"
|
||||
command mkfifo "$fifo" || exit 1
|
||||
|
||||
cleanup() {
|
||||
kill $killer_pid 2>/dev/null || true
|
||||
kill $writer_pid 2>/dev/null || true
|
||||
wait $writer_pid 2>/dev/null || true
|
||||
command rm -rf "$tmpdir"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
(
|
||||
{
|
||||
print -rn -- "BIG="
|
||||
command dd if=/dev/zero bs=10485761 count=1 2>/dev/null | tr "\0" a
|
||||
} > "$fifo"
|
||||
) &
|
||||
writer_pid=$!
|
||||
|
||||
(
|
||||
sleep 2
|
||||
kill -0 $$ 2>/dev/null || exit 0
|
||||
kill $$ 2>/dev/null || exit 0
|
||||
) &
|
||||
killer_pid=$!
|
||||
|
||||
ZSH_DOTENV_PROMPT=false
|
||||
ZSH_DOTENV_FILE="$fifo"
|
||||
source_env >/dev/null 2>&1
|
||||
'
|
||||
|
||||
assert $state equals 1
|
||||
}
|
||||
|
||||
@test 'parse basic variable assignment' {
|
||||
> "$fixture" <<'EOF'
|
||||
# Basic assignments
|
||||
BASIC=basic
|
||||
|
||||
# previous line intentionally left blank
|
||||
AFTER_LINE=after_line
|
||||
EOF
|
||||
|
||||
expected_vars=(
|
||||
BASIC 'basic'
|
||||
AFTER_LINE 'after_line'
|
||||
)
|
||||
|
||||
_parse_dotenv_test "$fixture"
|
||||
|
||||
assert "DOTENV_TEST_VARS" var_same_as "expected_vars"
|
||||
}
|
||||
|
||||
@test 'parse empty values' {
|
||||
> "$fixture" <<'EOF'
|
||||
# Empty values
|
||||
EMPTY=
|
||||
EMPTY_SINGLE_QUOTES=''
|
||||
EMPTY_DOUBLE_QUOTES=""
|
||||
EOF
|
||||
|
||||
expected_vars=(
|
||||
EMPTY ''
|
||||
EMPTY_SINGLE_QUOTES ''
|
||||
EMPTY_DOUBLE_QUOTES ''
|
||||
)
|
||||
|
||||
_parse_dotenv_test "$fixture"
|
||||
|
||||
assert "DOTENV_TEST_VARS" var_same_as "expected_vars"
|
||||
}
|
||||
|
||||
@test 'parse single quoted values' {
|
||||
> "$fixture" <<'EOF'
|
||||
# Single quotes (literal, no expansion)
|
||||
SINGLE_QUOTES='single_quotes'
|
||||
SINGLE_QUOTES_SPACED=' single quotes '
|
||||
DONT_EXPAND_SQUOTED='dontexpand\nnewlines'
|
||||
EOF
|
||||
|
||||
expected_vars=(
|
||||
SINGLE_QUOTES 'single_quotes'
|
||||
SINGLE_QUOTES_SPACED ' single quotes '
|
||||
DONT_EXPAND_SQUOTED 'dontexpand\nnewlines'
|
||||
)
|
||||
|
||||
_parse_dotenv_test "$fixture"
|
||||
|
||||
assert "DOTENV_TEST_VARS" var_same_as "expected_vars"
|
||||
}
|
||||
|
||||
@test 'parse double quoted values' {
|
||||
> "$fixture" <<'EOF'
|
||||
# Double quotes (with escapes)
|
||||
DOUBLE_QUOTES="double_quotes"
|
||||
DOUBLE_QUOTES_SPACED=" double quotes "
|
||||
EXPAND_NEWLINES="expand\nnew\nlines"
|
||||
EOF
|
||||
|
||||
expected_vars=(
|
||||
DOUBLE_QUOTES 'double_quotes'
|
||||
DOUBLE_QUOTES_SPACED ' double quotes '
|
||||
EXPAND_NEWLINES "expand\nnew\nlines"
|
||||
)
|
||||
|
||||
_parse_dotenv_test "$fixture"
|
||||
|
||||
assert "DOTENV_TEST_VARS" var_same_as "expected_vars"
|
||||
}
|
||||
|
||||
@test 'parse unquoted values' {
|
||||
> "$fixture" <<'EOF'
|
||||
# Unquoted (no escape expansion)
|
||||
DONT_EXPAND_UNQUOTED=dontexpand\\nnewlines
|
||||
EOF
|
||||
|
||||
|
||||
expected_vars=(
|
||||
DONT_EXPAND_UNQUOTED 'dontexpandnnewlines'
|
||||
)
|
||||
|
||||
_parse_dotenv_test "$fixture"
|
||||
|
||||
assert "DOTENV_TEST_VARS" var_same_as "expected_vars"
|
||||
}
|
||||
|
||||
@test 'parse quotes inside quotes' {
|
||||
> "$fixture" <<'EOF'
|
||||
# Quotes inside quotes
|
||||
DOUBLE_QUOTES_INSIDE_SINGLE='double "quotes" work inside single quotes'
|
||||
SINGLE_QUOTES_INSIDE_DOUBLE="single 'quotes' work inside double quotes"
|
||||
EOF
|
||||
|
||||
expected_vars=(
|
||||
DOUBLE_QUOTES_INSIDE_SINGLE 'double "quotes" work inside single quotes'
|
||||
SINGLE_QUOTES_INSIDE_DOUBLE "single 'quotes' work inside double quotes"
|
||||
)
|
||||
|
||||
_parse_dotenv_test "$fixture"
|
||||
|
||||
assert "DOTENV_TEST_VARS" var_same_as "expected_vars"
|
||||
}
|
||||
|
||||
@test 'parse inline comments' {
|
||||
> "$fixture" <<'EOF'
|
||||
# Comments
|
||||
# COMMENTS=work
|
||||
INLINE_COMMENTS_SINGLE_QUOTES='inline comments outside of #singlequotes' # work
|
||||
INLINE_COMMENTS_DOUBLE_QUOTES="inline comments outside of #doublequotes" # work
|
||||
INLINE_COMMENTS_UNQUOTED=value # work
|
||||
EOF
|
||||
|
||||
expected_vars=(
|
||||
INLINE_COMMENTS_SINGLE_QUOTES 'inline comments outside of #singlequotes'
|
||||
INLINE_COMMENTS_DOUBLE_QUOTES 'inline comments outside of #doublequotes'
|
||||
INLINE_COMMENTS_UNQUOTED 'value'
|
||||
)
|
||||
|
||||
_parse_dotenv_test "$fixture"
|
||||
|
||||
assert "DOTENV_TEST_VARS" var_same_as "expected_vars"
|
||||
}
|
||||
|
||||
@test 'parse ignores non-assignment commands with assignment-looking arguments' {
|
||||
> "$fixture" <<'EOF'
|
||||
print SHOULD_NOT_PARSE=value
|
||||
EOF
|
||||
|
||||
_parse_dotenv_test "$fixture"
|
||||
|
||||
assert "DOTENV_TEST_VARS" var_same_as "expected_vars"
|
||||
}
|
||||
|
||||
@test 'parse special characters' {
|
||||
> "$fixture" <<'EOF'
|
||||
# Special characters
|
||||
EQUAL_SIGNS=equals==
|
||||
RETAIN_INNER_QUOTES_AS_STRING='{"foo": "bar"}'
|
||||
USEREMAIL=therealnerdybeast@example.tld
|
||||
EOF
|
||||
|
||||
expected_vars=(
|
||||
EQUAL_SIGNS 'equals=='
|
||||
RETAIN_INNER_QUOTES_AS_STRING '{"foo": "bar"}'
|
||||
USEREMAIL 'therealnerdybeast@example.tld'
|
||||
)
|
||||
|
||||
_parse_dotenv_test "$fixture"
|
||||
|
||||
assert "DOTENV_TEST_VARS" var_same_as "expected_vars"
|
||||
}
|
||||
|
||||
@test 'parse multiline values with mixed quotes' {
|
||||
> "$fixture" <<'EOF'
|
||||
# Multiline values with double quotes
|
||||
MULTI_DOUBLE_QUOTED="THIS
|
||||
IS
|
||||
A
|
||||
MULTILINE
|
||||
STRING"
|
||||
|
||||
|
||||
# Multiline values with single quotes
|
||||
MULTI_SINGLE_QUOTED='THIS
|
||||
IS
|
||||
A
|
||||
MULTILINE
|
||||
STRING'
|
||||
|
||||
# Multiline PEM certificate
|
||||
MULTI_PEM_DOUBLE_QUOTED="-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnNl1tL3QjKp3DZWM0T3u
|
||||
LgGJQwu9WqyzHKZ6WIA5T+7zPjO1L8l3S8k8YzBrfH4mqWOD1GBI8Yjq2L1ac3Y/
|
||||
bTdfHN8CmQr2iDJC0C6zY8YV93oZB3x0zC/LPbRYpF8f6OqX1lZj5vo2zJZy4fI/
|
||||
kKcI5jHYc8VJq+KCuRZrvn+3V+KuL9tF9v8ZgjF2PZbU+LsCy5Yqg1M8f5Jp5f6V
|
||||
u4QuUoobAgMBAAE=
|
||||
-----END PUBLIC KEY-----"
|
||||
EOF
|
||||
|
||||
expected_vars=(
|
||||
MULTI_DOUBLE_QUOTED $'THIS\nIS\nA\nMULTILINE\nSTRING'
|
||||
MULTI_SINGLE_QUOTED $'THIS\nIS\nA\nMULTILINE\nSTRING'
|
||||
MULTI_PEM_DOUBLE_QUOTED $'-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnNl1tL3QjKp3DZWM0T3u\nLgGJQwu9WqyzHKZ6WIA5T+7zPjO1L8l3S8k8YzBrfH4mqWOD1GBI8Yjq2L1ac3Y/\nbTdfHN8CmQr2iDJC0C6zY8YV93oZB3x0zC/LPbRYpF8f6OqX1lZj5vo2zJZy4fI/\nkKcI5jHYc8VJq+KCuRZrvn+3V+KuL9tF9v8ZgjF2PZbU+LsCy5Yqg1M8f5Jp5f6V\nu4QuUoobAgMBAAE=\n-----END PUBLIC KEY-----'
|
||||
)
|
||||
|
||||
_parse_dotenv_test "$fixture"
|
||||
|
||||
assert "DOTENV_TEST_VARS" var_same_as "expected_vars"
|
||||
}
|
||||
|
||||
@test 'parse export syntax' {
|
||||
> "$fixture" <<'EOF'
|
||||
# Exported variables
|
||||
export EXPORTED_VAR=exported_value
|
||||
export EXPORTED_EMPTY=
|
||||
EOF
|
||||
|
||||
expected_vars=(
|
||||
EXPORTED_VAR 'exported_value'
|
||||
EXPORTED_EMPTY ''
|
||||
)
|
||||
|
||||
_parse_dotenv_test "$fixture"
|
||||
|
||||
assert "DOTENV_TEST_VARS" var_same_as "expected_vars"
|
||||
}
|
||||
|
||||
@test 'parse in-file variable expansion' {
|
||||
> "$fixture" <<'EOF'
|
||||
# Variable expansion (in-file forward references)
|
||||
BASE_URL=https://api.example.com
|
||||
API_ENDPOINT="${BASE_URL}/v1"
|
||||
FULL_ENDPOINT=$BASE_URL/v2/users
|
||||
COMBINED="${BASE_URL}_suffix"
|
||||
EOF
|
||||
|
||||
expected_vars=(
|
||||
BASE_URL 'https://api.example.com'
|
||||
API_ENDPOINT 'https://api.example.com/v1'
|
||||
FULL_ENDPOINT 'https://api.example.com/v2/users'
|
||||
COMBINED 'https://api.example.com_suffix'
|
||||
)
|
||||
|
||||
_parse_dotenv_test "$fixture"
|
||||
|
||||
assert "DOTENV_TEST_VARS" var_same_as "expected_vars"
|
||||
}
|
||||
|
||||
@test 'parse in-file variable expansion prefers the longest matching variable name' {
|
||||
> "$fixture" <<'EOF'
|
||||
A=1
|
||||
ABC=2
|
||||
X=$ABC
|
||||
Y=${ABC}
|
||||
Z=$ABCD
|
||||
EOF
|
||||
|
||||
expected_vars=(
|
||||
A '1'
|
||||
ABC '2'
|
||||
X '2'
|
||||
Y '2'
|
||||
Z ''
|
||||
)
|
||||
|
||||
_parse_dotenv_test "$fixture"
|
||||
|
||||
assert "DOTENV_TEST_VARS" var_same_as "expected_vars"
|
||||
}
|
||||
|
||||
@test 'parse preserves escaped dollar signs before variable expansion' {
|
||||
> "$fixture" <<'EOF'
|
||||
BAR=expanded
|
||||
ESCAPED_UNQUOTED=foo\$BAR
|
||||
ESCAPED_DOUBLE="foo\$BAR"
|
||||
ESCAPED_BRACED="\${BAR}"
|
||||
EOF
|
||||
|
||||
expected_vars=(
|
||||
BAR 'expanded'
|
||||
ESCAPED_UNQUOTED 'foo$BAR'
|
||||
ESCAPED_DOUBLE 'foo$BAR'
|
||||
ESCAPED_BRACED '${BAR}'
|
||||
)
|
||||
|
||||
_parse_dotenv_test "$fixture"
|
||||
|
||||
assert "DOTENV_TEST_VARS" var_same_as "expected_vars"
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env zunit
|
||||
|
||||
@setup {
|
||||
unset DOTENV_TEST_VARS DOTENV_SOURCE_VARS 2>/dev/null
|
||||
}
|
||||
|
||||
@teardown {
|
||||
unset DOTENV_TEST_VARS DOTENV_SOURCE_VARS 2>/dev/null
|
||||
}
|
||||
|
||||
@test 'compatibility: dotenvjs fixture matches native source' {
|
||||
local fixture="${testdir:A}/_support/fixtures/dotenvjs.env"
|
||||
|
||||
_parse_dotenv_test "$fixture"
|
||||
_source_with_allexport "$fixture"
|
||||
|
||||
assert "DOTENV_TEST_VARS" var_same_as "DOTENV_SOURCE_VARS"
|
||||
}
|
||||
|
||||
@test 'compatibility: features fixture matches native source' {
|
||||
local fixture="${testdir:A}/_support/fixtures/features.env"
|
||||
|
||||
_parse_dotenv_test "$fixture"
|
||||
_source_with_allexport "$fixture"
|
||||
|
||||
assert "DOTENV_TEST_VARS" var_same_as "DOTENV_SOURCE_VARS"
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
#!/usr/bin/env zunit
|
||||
|
||||
@setup {
|
||||
typeset -g fixture="$(_create_temp_fixture)"
|
||||
typeset -gA expected_vars=()
|
||||
}
|
||||
|
||||
@teardown {
|
||||
[[ -f "$fixture" ]] && command rm -f "$fixture"
|
||||
unset DOTENV_TEST_VARS DOTENV_SOURCE_VARS 2>/dev/null
|
||||
}
|
||||
|
||||
@test 'skip dangerous backtick command substitution' {
|
||||
> "$fixture" <<'EOF'
|
||||
# Should be skipped
|
||||
DANGEROUS_BACKTICK=`whoami`
|
||||
EOF
|
||||
|
||||
_parse_dotenv_test "$fixture"
|
||||
|
||||
assert "DOTENV_TEST_VARS" var_same_as "expected_vars"
|
||||
}
|
||||
|
||||
@test 'skip dangerous subshell command substitution' {
|
||||
> "$fixture" <<'EOF'
|
||||
# Should be skipped
|
||||
DANGEROUS_SUBSHELL=$(date)
|
||||
EOF
|
||||
|
||||
_parse_dotenv_test "$fixture"
|
||||
|
||||
assert "DOTENV_TEST_VARS" var_same_as "expected_vars"
|
||||
}
|
||||
|
||||
@test 'skip nested command substitution in double quotes' {
|
||||
> "$fixture" <<'EOF'
|
||||
# Should be skipped
|
||||
DANGEROUS_NESTED="prefix_$(echo malicious)_suffix"
|
||||
EOF
|
||||
|
||||
_parse_dotenv_test "$fixture"
|
||||
|
||||
assert "DOTENV_TEST_VARS" var_same_as "expected_vars"
|
||||
}
|
||||
|
||||
@test 'skip multiple words (potential command execution)' {
|
||||
> "$fixture" <<'EOF'
|
||||
# Should be skipped - multiple words could execute commands
|
||||
BASE_URL=/ echo command run
|
||||
EOF
|
||||
|
||||
_parse_dotenv_test "$fixture"
|
||||
|
||||
assert "DOTENV_TEST_VARS" var_same_as "expected_vars"
|
||||
}
|
||||
|
||||
@test 'allow literal command substitution in single quotes' {
|
||||
> "$fixture" <<'EOF'
|
||||
# Single quotes make everything literal - should be parsed
|
||||
SAFE_SINGLE_QUOTED='$(this is literal)'
|
||||
SAFE_BACKTICK='`also literal`'
|
||||
|
||||
# Should also be parsed
|
||||
SAFE_VAR=safe_value
|
||||
EOF
|
||||
|
||||
expected_vars=(
|
||||
SAFE_SINGLE_QUOTED '$(this is literal)'
|
||||
SAFE_BACKTICK '`also literal`'
|
||||
SAFE_VAR 'safe_value'
|
||||
)
|
||||
|
||||
_parse_dotenv_test "$fixture"
|
||||
|
||||
assert "DOTENV_TEST_VARS" var_same_as "expected_vars"
|
||||
}
|
||||
|
||||
@test 'skip backticks in unquoted values' {
|
||||
> "$fixture" <<'EOF'
|
||||
# Backticks in unquoted context - should be skipped
|
||||
DANGEROUS_UNQUOTED=`echo danger`
|
||||
EOF
|
||||
|
||||
_parse_dotenv_test "$fixture"
|
||||
|
||||
assert "DOTENV_TEST_VARS" var_same_as "expected_vars"
|
||||
}
|
||||
|
||||
@test 'skip dollar-paren in unquoted values' {
|
||||
> "$fixture" <<'EOF'
|
||||
# Command substitution in unquoted context - should be skipped
|
||||
DANGEROUS_UNQUOTED=$(uname -a)
|
||||
EOF
|
||||
|
||||
_parse_dotenv_test "$fixture"
|
||||
|
||||
assert "DOTENV_TEST_VARS" var_same_as "expected_vars"
|
||||
}
|
||||
|
||||
@test 'allow safe dollar signs (variable refs without parens in single quotes)' {
|
||||
> "$fixture" <<'EOF'
|
||||
# Dollar signs that don't start command substitution
|
||||
SAFE_DOLLARS='$HOME is literal'
|
||||
SAFE_PRICE='Cost is $50'
|
||||
SAFE_VAR='value$123'
|
||||
|
||||
# Should all be parsed
|
||||
SAFE_VAR2=safe_value
|
||||
EOF
|
||||
|
||||
expected_vars=(
|
||||
SAFE_DOLLARS '$HOME is literal'
|
||||
SAFE_PRICE 'Cost is $50'
|
||||
SAFE_VAR 'value$123'
|
||||
SAFE_VAR2 'safe_value'
|
||||
)
|
||||
|
||||
_parse_dotenv_test "$fixture"
|
||||
|
||||
assert "DOTENV_TEST_VARS" var_same_as "expected_vars"
|
||||
}
|
||||
|
||||
@test 'skip quoted command substitution' {
|
||||
> "$fixture" <<'EOF'
|
||||
HARMLESS_COMMAND="\$(echo)"
|
||||
ANOTHER_ONE=$'\x24\x28echo\x29'
|
||||
EOF
|
||||
|
||||
_parse_dotenv_test "$fixture"
|
||||
|
||||
assert "DOTENV_TEST_VARS" var_same_as "expected_vars"
|
||||
}
|
||||
|
||||
@test 'comprehensive security test with mixed safe and dangerous patterns' {
|
||||
> "$fixture" <<'EOF'
|
||||
# These should be SKIPPED (dangerous)
|
||||
DANGEROUS_BACKTICK=`whoami`
|
||||
DANGEROUS_SUBSHELL=$(date)
|
||||
DANGEROUS_NESTED="prefix_$(echo malicious)_suffix"
|
||||
LOOKS_SAFE=$(curl http://evil.com)
|
||||
BASE_URL=/ echo command run
|
||||
|
||||
# These should WORK (safe)
|
||||
SAFE_BEFORE=safe_value_1
|
||||
SAFE_AFTER=safe_value_2
|
||||
SAFE_SINGLE_QUOTED='$(this is literal)'
|
||||
SAFE_SINGLE_QUOTED2='`also literal`'
|
||||
SAFE_DOLLARS='$HOME'
|
||||
SAFE_PRICE="$50"
|
||||
EOF
|
||||
|
||||
expected_vars=(
|
||||
SAFE_BEFORE 'safe_value_1'
|
||||
SAFE_AFTER 'safe_value_2'
|
||||
SAFE_SINGLE_QUOTED '$(this is literal)'
|
||||
SAFE_SINGLE_QUOTED2 '`also literal`'
|
||||
SAFE_DOLLARS '$HOME'
|
||||
SAFE_PRICE '$50'
|
||||
)
|
||||
|
||||
_parse_dotenv_test "$fixture"
|
||||
|
||||
assert "DOTENV_TEST_VARS" var_same_as "expected_vars"
|
||||
}
|
||||
|
||||
|
||||
|
||||
@test 'blocks changes of special environment variables' {
|
||||
_parse_dotenv_test =(<<'EOF'
|
||||
# Executes on the next node/npm/npx invocation
|
||||
NODE_OPTIONS=--require=./payload.js
|
||||
|
||||
# Used for shell initialization
|
||||
BASH_ENV=./payload.sh
|
||||
# Used for shell initialization in zsh, but also respected by some tools like git
|
||||
# - https://man7.org/linux/man-pages/man1/dash.1.html#DESCRIPTION:~:text=by%20the%20shell.-,Invocation,-If%20no%20args
|
||||
# - https://zsh.sourceforge.io/Doc/Release/Parameters.html#index-ENV
|
||||
ENV=./payload.sh
|
||||
# Used for zsh startup
|
||||
ZDOTDIR=./.malicious_zsh
|
||||
ZSH=./.malicious_zsh
|
||||
|
||||
# These are used for native code injection
|
||||
LD_PRELOAD=./payload.so
|
||||
LD_LIBRARY_PATH=./malicious_libs
|
||||
DYLD_INSERT_LIBRARIES=./payload.dylib
|
||||
|
||||
# Git environment variables
|
||||
GIT_CONFIG_GLOBAL=./.gitconfig-malicious
|
||||
GIT_DIR=./malicious_git_dir
|
||||
GIT_EDITOR=./malicious_editor
|
||||
GIT_EXTERNAL_DIFF=./malicious_diff
|
||||
GIT_EXEC_PATH=./.malicious_git_exec
|
||||
GIT_PAGER=./malicious_pager
|
||||
GIT_SSH=./malicious_ssh
|
||||
GIT_SSH_COMMAND=./malicious_ssh_command
|
||||
GIT_SSL_NO_VERIFY=true
|
||||
GIT_TEMPLATE_DIR=./malicious_templates # for persistence
|
||||
|
||||
# Special exported variables
|
||||
PATH=./malicious_bin:$PATH
|
||||
EDITOR=./malicious
|
||||
VISUAL=./malicious
|
||||
PAGER=./malicious
|
||||
EOF
|
||||
)
|
||||
|
||||
assert "DOTENV_TEST_VARS" var_same_as "expected_vars"
|
||||
}
|
||||
@@ -53,5 +53,6 @@ local -a exts=(
|
||||
|
||||
_arguments \
|
||||
'(-r --remove)'{-r,--remove}'[Remove archive.]' \
|
||||
'(-t --to-directory)'{-t,--to-directory}'[Extract to a specific directory.]' \
|
||||
"*::archive file:_files -g '(#i)*.(${(j:|:)exts})(-.)'" \
|
||||
&& return 0
|
||||
|
||||
Regular → Executable
+40
-9
@@ -9,14 +9,41 @@ Usage: extract [-option] [file ...]
|
||||
|
||||
Options:
|
||||
-r, --remove Remove archive after unpacking.
|
||||
-t, --to-directory <dir> Extract to a specific directory instead of the current one.
|
||||
EOF
|
||||
fi
|
||||
|
||||
local remove_archive=1
|
||||
if [[ "$1" == "-r" ]] || [[ "$1" == "--remove" ]]; then
|
||||
remove_archive=0
|
||||
shift
|
||||
fi
|
||||
local target_directory=""
|
||||
|
||||
while (( $# > 0 )); do
|
||||
case "$1" in
|
||||
-r|--remove)
|
||||
remove_archive=0
|
||||
shift
|
||||
;;
|
||||
-t|--to-directory)
|
||||
shift
|
||||
if (( $# == 0 )); then
|
||||
echo "extract: -t/--to-directory requires a directory argument" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
target_directory="$1"
|
||||
shift
|
||||
|
||||
if [[ ! -d "$target_directory" ]]; then
|
||||
echo "extract: '$target_directory' is not a valid directory" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
target_directory="${target_directory%/}"
|
||||
;;
|
||||
*)
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
local pwd="$PWD"
|
||||
while (( $# > 0 )); do
|
||||
@@ -35,6 +62,10 @@ EOF
|
||||
extract_dir="${extract_dir:r}"
|
||||
fi
|
||||
|
||||
if [[ -n "$target_directory" ]]; then
|
||||
extract_dir="$target_directory/${extract_dir:t}"
|
||||
fi
|
||||
|
||||
# If there's a file or directory with the same name as the archive
|
||||
# add a random string to the end of the extract directory
|
||||
if [[ -e "$extract_dir" ]]; then
|
||||
@@ -71,7 +102,7 @@ EOF
|
||||
(*.tar.lrz) (( $+commands[lrzuntar] )) && lrzuntar "$full_path" ;;
|
||||
(*.gz) (( $+commands[pigz] )) && pigz -cdk "$full_path" > "${file:t:r}" || gunzip -ck "$full_path" > "${file:t:r}" ;;
|
||||
(*.bz2) (( $+commands[pbzip2] )) && pbzip2 -d "$full_path" || bunzip2 "$full_path" ;;
|
||||
(*.xz) unxz "$full_path" ;;
|
||||
(*.xz) xzcat "$full_path" > "${file:t:r}" ;;
|
||||
(*.lrz) (( $+commands[lrunzip] )) && lrunzip "$full_path" ;;
|
||||
(*.lz4) lz4 -d "$full_path" ;;
|
||||
(*.lzma) unlzma "$full_path" ;;
|
||||
@@ -126,7 +157,7 @@ EOF
|
||||
# 1. Move and rename the extracted file/folder to a temporary random name
|
||||
# 2. Delete the empty folder
|
||||
# 3. Rename the extracted file/folder to the original name
|
||||
if [[ "${content[1]:t}" == "$extract_dir" ]]; then
|
||||
if [[ "${content[1]:t}" == "${extract_dir:t}" ]]; then
|
||||
# =(:) gives /tmp/zsh<random>, with :t it gives zsh<random>
|
||||
local tmp_name==(:); tmp_name="${tmp_name:t}"
|
||||
command mv "${content[1]}" "$tmp_name" \
|
||||
@@ -134,9 +165,9 @@ EOF
|
||||
&& command mv "$tmp_name" "$extract_dir"
|
||||
# Otherwise, if the extracted folder name already exists in the current
|
||||
# directory (because of a previous file / folder), keep the extract_dir
|
||||
elif [[ ! -e "${content[1]:t}" ]]; then
|
||||
command mv "${content[1]}" . \
|
||||
&& command rmdir "$extract_dir"
|
||||
elif [[ ! -e "${target_directory:-.}/${content[1]:t}" ]]; then
|
||||
command mv -- "${content[1]}" "${target_directory:-.}/" \
|
||||
&& command rmdir -- "$extract_dir"
|
||||
fi
|
||||
elif [[ ${#content} -eq 0 ]]; then
|
||||
command rmdir "$extract_dir"
|
||||
|
||||
@@ -48,10 +48,11 @@ Default: `no`
|
||||
### `show-group`
|
||||
|
||||
```zsh
|
||||
zstyle ':omz:plugins:eza' 'show-group' yes|no
|
||||
zstyle ':omz:plugins:eza' 'show-group' yes|no|smart
|
||||
```
|
||||
|
||||
If `yes` (default), always add `-g` flag to show the group ownership.
|
||||
If `smart`, adds the `--smart-group` flag to only show the group if it has a different name from the owner.
|
||||
|
||||
Default: `yes`
|
||||
|
||||
|
||||
@@ -9,9 +9,15 @@ typeset -a _EZA_TAIL
|
||||
function _configure_eza() {
|
||||
local _val
|
||||
# Get the head flags
|
||||
if zstyle -T ':omz:plugins:eza' 'show-group'; then
|
||||
_EZA_HEAD+=("g")
|
||||
fi
|
||||
zstyle -s ':omz:plugins:eza' 'show-group' _val
|
||||
case "${_val:l}" in
|
||||
yes)
|
||||
_EZA_HEAD+=("g")
|
||||
;;
|
||||
smart)
|
||||
_EZA_TAIL+=("--smart-group")
|
||||
;;
|
||||
esac
|
||||
if zstyle -t ':omz:plugins:eza' 'header'; then
|
||||
_EZA_HEAD+=("h")
|
||||
fi
|
||||
|
||||
@@ -13,16 +13,20 @@ plugins=(... flutter)
|
||||
| Alias | Command | Description |
|
||||
| :--------- | :---------------------- | :------------------------------------------------------------------------- |
|
||||
| `fl` | `flutter` | Shorthand for flutter command |
|
||||
| `fla` | `flutter analyze` | Analyzes flutter code |
|
||||
| `flattach` | `flutter attach` | Attaches flutter to a running flutter application with enabled observatory |
|
||||
| `flb` | `flutter build` | Build flutter application |
|
||||
| `flchnl` | `flutter channel` | Switches flutter channel (requires input of desired channel) |
|
||||
| `flc` | `flutter clean` | Cleans flutter project |
|
||||
| `fldvcs` | `flutter devices` | List connected devices (if any) |
|
||||
| `flchnl` | `flutter channel` | Switches flutter channel (requires input of desired channel) |
|
||||
| `fldoc` | `flutter doctor` | Runs flutter doctor |
|
||||
| `flpub` | `flutter pub` | Shorthand for flutter pub command |
|
||||
| `fldvcs` | `flutter devices` | List connected devices (if any) |
|
||||
| `flget` | `flutter pub get` | Installs dependencies |
|
||||
| `fll` | `flutter logs` | Shows flutter logs |
|
||||
| `flpu` | `flutter pub upgrade` | Upgrades dependencies |
|
||||
| `flpub` | `flutter pub` | Shorthand for flutter pub command |
|
||||
| `flr` | `flutter run` | Runs flutter app |
|
||||
| `flrd` | `flutter run --debug` | Runs flutter app in debug mode (default mode) |
|
||||
| `flrp` | `flutter run --profile` | Runs flutter app in profile mode |
|
||||
| `flrr` | `flutter run --release` | Runs flutter app in release mode |
|
||||
| `flt` | `flutter test` | Runs flutter tests |
|
||||
| `flupgrd` | `flutter upgrade` | Upgrades flutter version depending on the current channel |
|
||||
@@ -1,16 +1,20 @@
|
||||
alias fl="flutter"
|
||||
alias fla="flutter analyze"
|
||||
alias flattach="flutter attach"
|
||||
alias flb="flutter build"
|
||||
alias flchnl="flutter channel"
|
||||
alias flc="flutter clean"
|
||||
alias fldvcs="flutter devices"
|
||||
alias flchnl="flutter channel"
|
||||
alias fldoc="flutter doctor"
|
||||
alias flpub="flutter pub"
|
||||
alias fldvcs="flutter devices"
|
||||
alias flget="flutter pub get"
|
||||
alias fll="flutter logs"
|
||||
alias flpu="flutter pub upgrade"
|
||||
alias flpub="flutter pub"
|
||||
alias flr="flutter run"
|
||||
alias flrd="flutter run --debug"
|
||||
alias flrp="flutter run --profile"
|
||||
alias flrr="flutter run --release"
|
||||
alias flt="flutter test"
|
||||
alias flupgrd="flutter upgrade"
|
||||
|
||||
# COMPLETION FUNCTION
|
||||
|
||||
@@ -45,7 +45,7 @@ plugins=(... git)
|
||||
| `gbgD` | `LANG=C git branch --no-color -vv \| grep ": gone\]" \| cut -c 3- \| awk '"'"'{print $1}'"'"' \| xargs git branch -D` |
|
||||
| `gbm` | `git branch --move` |
|
||||
| `gbnm` | `git branch --no-merged` |
|
||||
| `gbr` | `git branch --remote` |
|
||||
| `gbr` | `git branch --remotes` |
|
||||
| `ggsup` | `git branch --set-upstream-to=origin/$(git_current_branch)` |
|
||||
| `gbg` | `LANG=C git branch -vv \| grep ": gone\]"` |
|
||||
| `gco` | `git checkout` |
|
||||
@@ -181,6 +181,8 @@ plugins=(... git)
|
||||
| `grst` | `git restore --staged` |
|
||||
| `gunwip` | `git rev-list --max-count=1 --format="%s" HEAD \| grep -q "--wip--" && git reset HEAD~1` |
|
||||
| `grev` | `git revert` |
|
||||
| `greva` | `git revert --abort` |
|
||||
| `grevc` | `git revert --continue` |
|
||||
| `grm` | `git rm` |
|
||||
| `grmc` | `git rm --cached` |
|
||||
| `gcount` | `git shortlog --summary -n` |
|
||||
@@ -215,6 +217,7 @@ plugins=(... git)
|
||||
| `gunignore` | `git update-index --no-assume-unchanged` |
|
||||
| `gwch` | `git log --patch --abbrev-commit --pretty=medium --raw` |
|
||||
| `gwt` | `git worktree` |
|
||||
| `gwta` | `git worktree add` |
|
||||
| `gwtls` | `git worktree list` |
|
||||
| `gwtmv` | `git worktree move` |
|
||||
| `gwtrm` | `git worktree remove` |
|
||||
|
||||
@@ -153,7 +153,7 @@ alias gbgd='LANG=C git branch --no-color -vv | grep ": gone\]" | cut -c 3- | awk
|
||||
alias gbgD='LANG=C git branch --no-color -vv | grep ": gone\]" | cut -c 3- | awk '"'"'{print $1}'"'"' | xargs git branch -D'
|
||||
alias gbm='git branch --move'
|
||||
alias gbnm='git branch --no-merged'
|
||||
alias gbr='git branch --remote'
|
||||
alias gbr='git branch --remotes'
|
||||
alias ggsup='git branch --set-upstream-to=origin/$(git_current_branch)'
|
||||
alias gbg='LANG=C git branch -vv | grep ": gone\]"'
|
||||
alias gco='git checkout'
|
||||
|
||||
@@ -235,7 +235,7 @@ __git_ps1_show_upstream ()
|
||||
if [ $pcmode = yes ] && [ $ps1_expanded = yes ]; then
|
||||
upstream="$upstream \${__git_ps1_upstream_name}"
|
||||
else
|
||||
upstream="$upstream ${__git_ps1_upstream_name}"
|
||||
upstream="$upstream ${__git_ps1_upstream_name//\%/%%}"
|
||||
# not needed anymore; keep user's
|
||||
# environment clean
|
||||
unset __git_ps1_upstream_name
|
||||
@@ -570,6 +570,9 @@ __git_ps1 ()
|
||||
if [ $pcmode = yes ] && [ $ps1_expanded = yes ]; then
|
||||
__git_ps1_branch_name=$b
|
||||
b="\${__git_ps1_branch_name}"
|
||||
else
|
||||
# escape % in branch name to avoid prompt expansion issues
|
||||
b="${b//\%/%%}"
|
||||
fi
|
||||
|
||||
if [ -n "${GIT_PS1_SHOWCOLORHINTS-}" ]; then
|
||||
|
||||
@@ -10,12 +10,48 @@ function gi() {
|
||||
}
|
||||
|
||||
_gitignoreio_get_command_list() {
|
||||
setopt local_options pipe_fail
|
||||
_gi_curl "list" | tr "," "\n"
|
||||
}
|
||||
|
||||
_gitignoreio () {
|
||||
__gitignoreio_caching_policy() {
|
||||
local -a oldp
|
||||
oldp=("$1"(Nm+7))
|
||||
(($#oldp))
|
||||
}
|
||||
|
||||
_gitignoreio_retrieve_stale_cache() {
|
||||
zstyle -t ":completion:${curcontext}:" use-cache || return 1
|
||||
|
||||
local cache_dir
|
||||
zstyle -s ":completion:${curcontext}:" cache-path cache_dir
|
||||
: ${cache_dir:=${ZDOTDIR:-$HOME}/.zcompcache}
|
||||
|
||||
[[ -e "$cache_dir/gi-list" ]] || return 1
|
||||
. "$cache_dir/gi-list"
|
||||
}
|
||||
|
||||
_gitignoreio() {
|
||||
compset -P '*,'
|
||||
compadd -S '' $(_gitignoreio_get_command_list)
|
||||
|
||||
local cache_policy
|
||||
zstyle -s ":completion:${curcontext}:" cache-policy cache_policy
|
||||
if [[ -z "$cache_policy" ]]; then
|
||||
zstyle ":completion:${curcontext}:" cache-policy __gitignoreio_caching_policy
|
||||
fi
|
||||
|
||||
local -a _gi_list
|
||||
if _cache_invalid gi-list || ! _retrieve_cache gi-list; then
|
||||
local command_list
|
||||
if command_list="$(_gitignoreio_get_command_list)" && [[ -n "$command_list" ]]; then
|
||||
_gi_list=(${(f)command_list})
|
||||
_store_cache gi-list _gi_list
|
||||
else
|
||||
_gitignoreio_retrieve_stale_cache
|
||||
fi
|
||||
fi
|
||||
|
||||
compadd -S '' -a _gi_list
|
||||
}
|
||||
|
||||
compdef _gitignoreio gi
|
||||
@@ -27,7 +27,7 @@ The plugin also documents two other ways to do this:
|
||||
|
||||
1. Using a function wrapper, such that, for example, there exists a function
|
||||
named `ls` which calls `gls` instead. Since functions have a higher preference
|
||||
than commands, this ends up calling the GNU coreutil. It has also a higher
|
||||
than commands, this ends up calling the GNU coreutils. It has also a higher
|
||||
preference over shell builtins (`gecho` is called instead of the builtin `echo`).
|
||||
|
||||
2. Using an alias. This has an even higher preference than functions, but they
|
||||
|
||||
+47
-1
@@ -15,6 +15,52 @@ __go_identifiers() {
|
||||
compadd $(godoc -templates "$tmpl_path" ${words[-2]} 2> /dev/null)
|
||||
}
|
||||
|
||||
__go_tool_commands() {
|
||||
local -a tools tool_commands
|
||||
local -A command_seen short_count
|
||||
local tool command
|
||||
|
||||
tools=("${(@f)$(go tool 2>/dev/null)}")
|
||||
|
||||
# Go 1.24+ lists module tools by package path, but also accepts unique
|
||||
# default binary names for those tools.
|
||||
for tool in "${tools[@]}"; do
|
||||
[[ -n $tool ]] || continue
|
||||
|
||||
(( command_seen[$tool]++ ))
|
||||
|
||||
if [[ $tool == */* ]]; then
|
||||
command=${tool:t}
|
||||
|
||||
if [[ $command == v[0-9]* && ${command#v} != *[^0-9]* ]] && (( ${command#v} > 1 )); then
|
||||
command=${${tool%/$command}:t}
|
||||
fi
|
||||
|
||||
(( short_count[$command]++ ))
|
||||
fi
|
||||
done
|
||||
|
||||
for tool in "${tools[@]}"; do
|
||||
[[ -n $tool ]] || continue
|
||||
|
||||
tool_commands+=("$tool")
|
||||
|
||||
if [[ $tool == */* ]]; then
|
||||
command=${tool:t}
|
||||
|
||||
if [[ $command == v[0-9]* && ${command#v} != *[^0-9]* ]] && (( ${command#v} > 1 )); then
|
||||
command=${${tool%/$command}:t}
|
||||
fi
|
||||
|
||||
if (( short_count[$command] == 1 && ! command_seen[$command] )); then
|
||||
tool_commands+=("$command")
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
_values "go tool" "${tool_commands[@]}"
|
||||
}
|
||||
|
||||
_go() {
|
||||
typeset -a commands build_flags
|
||||
commands+=(
|
||||
@@ -208,7 +254,7 @@ _go() {
|
||||
;;
|
||||
tool)
|
||||
if (( CURRENT == 3 )); then
|
||||
_values "go tool" $(go tool)
|
||||
__go_tool_commands
|
||||
return
|
||||
fi
|
||||
case ${words[3]} in
|
||||
|
||||
@@ -269,6 +269,7 @@ __gradle_subcommand() {
|
||||
'-Dorg.gradle.configuration-cache.unsafe.ignore.unsupported-build-events-listeners=[]' \
|
||||
'-Dorg.gradle.configuration-cache=[Enables the configuration cache. Gradle will try to reuse the build configuration from previous builds.]' \
|
||||
'-Dorg.gradle.configureondemand=[Configures necessary projects only. Gradle will attempt to reduce configuration time for large multi-project builds.]' \
|
||||
'-Dorg.gradle.console.interactive=[]' \
|
||||
'-Dorg.gradle.console.unicode=[Specifies which character types are allowed in the console output. Supported values are 'auto' (default), 'disable', or 'enable'.]' \
|
||||
'-Dorg.gradle.console=[Specifies which type of console output to generate. Supported values are 'plain', 'colored', 'auto' (default), 'rich', or 'verbose'.]:org.gradle.console:(plain auto rich verbose)' \
|
||||
'-Dorg.gradle.continue=[Continues task execution after a task failure.]' \
|
||||
@@ -298,6 +299,7 @@ __gradle_subcommand() {
|
||||
'-Dorg.gradle.problems.report=[Enables the HTML problems report.]' \
|
||||
'-Dorg.gradle.projectcachedir=[Specifies the project-specific cache directory. Default is .gradle in the root project directory.]:org.gradle.projectcachedir:_directories' \
|
||||
'-Dorg.gradle.tooling.parallel=[]' \
|
||||
'-Dorg.gradle.unsafe.isolated-projects.diagnostics=[]' \
|
||||
'-Dorg.gradle.unsafe.isolated-projects=[]' \
|
||||
'-Dorg.gradle.vfs.verbose=[]' \
|
||||
'-Dorg.gradle.vfs.watch=[Enables file system watching. Reuses file system data for subsequent builds.]:org.gradle.vfs.watch:(true false)' \
|
||||
@@ -337,6 +339,7 @@ __gradle_subcommand() {
|
||||
{-a,--no-rebuild}'[Disables rebuilding of project dependencies.]' \
|
||||
(--scan)'--no-scan[Disables the creation of a Build Scan.]' \
|
||||
(--watch-fs)'--no-watch-fs[Disables file system watching.]' \
|
||||
'--non-interactive[Do not do interactive prompting. (incubating)]' \
|
||||
'--offline[Runs the build without accessing network resources.]' \
|
||||
(--no-parallel)'--parallel[Builds projects in parallel. Gradle will attempt to determine the optimal number of executor threads to use.]' \
|
||||
'--priority[Specifies the scheduling priority for the Gradle daemon and all processes launched by it. Supported values are 'normal' (default) or 'low'.]' \
|
||||
@@ -403,6 +406,7 @@ _gradle() {
|
||||
'-Dorg.gradle.configuration-cache.unsafe.ignore.unsupported-build-events-listeners=[]:->argument-expected' \
|
||||
'-Dorg.gradle.configuration-cache=[Enables the configuration cache. Gradle will try to reuse the build configuration from previous builds.]:->argument-expected' \
|
||||
'-Dorg.gradle.configureondemand=[Configures necessary projects only. Gradle will attempt to reduce configuration time for large multi-project builds.]:->argument-expected' \
|
||||
'-Dorg.gradle.console.interactive=[]:->argument-expected' \
|
||||
'-Dorg.gradle.console.unicode=[Specifies which character types are allowed in the console output. Supported values are 'auto' (default), 'disable', or 'enable'.]:->argument-expected' \
|
||||
'-Dorg.gradle.console=[Specifies which type of console output to generate. Supported values are 'plain', 'colored', 'auto' (default), 'rich', or 'verbose'.]:org.gradle.console:(plain auto rich verbose):->argument-expected' \
|
||||
'-Dorg.gradle.continue=[Continues task execution after a task failure.]:->argument-expected' \
|
||||
@@ -432,6 +436,7 @@ _gradle() {
|
||||
'-Dorg.gradle.problems.report=[Enables the HTML problems report.]:->argument-expected' \
|
||||
'-Dorg.gradle.projectcachedir=[Specifies the project-specific cache directory. Default is .gradle in the root project directory.]:org.gradle.projectcachedir:_directories:->argument-expected' \
|
||||
'-Dorg.gradle.tooling.parallel=[]:->argument-expected' \
|
||||
'-Dorg.gradle.unsafe.isolated-projects.diagnostics=[]:->argument-expected' \
|
||||
'-Dorg.gradle.unsafe.isolated-projects=[]:->argument-expected' \
|
||||
'-Dorg.gradle.vfs.verbose=[]:->argument-expected' \
|
||||
'-Dorg.gradle.vfs.watch=[Enables file system watching. Reuses file system data for subsequent builds.]:org.gradle.vfs.watch:(true false):->argument-expected' \
|
||||
@@ -472,6 +477,7 @@ _gradle() {
|
||||
{-a,--no-rebuild}'[Disables rebuilding of project dependencies.]' \
|
||||
(--scan)'--no-scan[Disables the creation of a Build Scan.]' \
|
||||
(--watch-fs)'--no-watch-fs[Disables file system watching.]' \
|
||||
'--non-interactive[Do not do interactive prompting. (incubating)]' \
|
||||
'--offline[Runs the build without accessing network resources.]' \
|
||||
(--no-parallel)'--parallel[Builds projects in parallel. Gradle will attempt to determine the optimal number of executor threads to use.]' \
|
||||
'--priority[Specifies the scheduling priority for the Gradle daemon and all processes launched by it. Supported values are 'normal' (default) or 'low'.]:->argument-expected' \
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
#
|
||||
# Enable caching:
|
||||
#
|
||||
# If you want to use the cache, set the followings in your .zshrc:
|
||||
# If you want to use the cache, set the following in your .zshrc:
|
||||
#
|
||||
# zstyle ':completion:*' use-cache yes
|
||||
#
|
||||
|
||||
@@ -20,7 +20,7 @@ Requirements
|
||||
Install
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
Using the [Homebrew]( https://brew.sh ) package manager:
|
||||
Using the [Homebrew](https://brew.sh) package manager:
|
||||
|
||||
brew install zsh-history-substring-search
|
||||
echo 'source $(brew --prefix)/share/zsh-history-substring-search/zsh-history-substring-search.zsh' >> ~/.zshrc
|
||||
|
||||
@@ -295,8 +295,8 @@ _history-substring-search-begin() {
|
||||
fi
|
||||
|
||||
#
|
||||
# Escape and join query parts with wildcard character '*' as seperator
|
||||
# `(j:CHAR:)` join array to string with CHAR as seperator
|
||||
# Escape and join query parts with wildcard character '*' as separator
|
||||
# `(j:CHAR:)` join array to string with CHAR as separator
|
||||
#
|
||||
local search_pattern="${(j:*:)_history_substring_search_query_parts[@]//(#m)[\][()|\\*?#<>~^]/\\$MATCH}*"
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ plugins=(... jj)
|
||||
| ------ | ----------------------------- |
|
||||
| jja | `jj abandon` |
|
||||
| jjb | `jj bookmark` |
|
||||
| jjba | `jj bookmark advance` |
|
||||
| jjbc | `jj bookmark create` |
|
||||
| jjbd | `jj bookmark delete` |
|
||||
| jjbf | `jj bookmark forget` |
|
||||
|
||||
@@ -36,6 +36,7 @@ function jj_prompt_template() {
|
||||
# Aliases (sorted alphabetically)
|
||||
alias jja='jj abandon'
|
||||
alias jjb='jj bookmark'
|
||||
alias jjba='jj bookmark advance'
|
||||
alias jjbc='jj bookmark create'
|
||||
alias jjbd='jj bookmark delete'
|
||||
alias jjbf='jj bookmark forget'
|
||||
@@ -69,3 +70,6 @@ alias jjrt='cd "$(jj root || echo .)"'
|
||||
alias jjsp='jj split'
|
||||
alias jjsq='jj squash'
|
||||
alias jjst='jj status'
|
||||
alias jjwa='jj workspace add'
|
||||
alias jjwf='jj workspace forget'
|
||||
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
#compdef juju
|
||||
(( $+functions[compdef] )) && compdef _juju juju
|
||||
|
||||
# zsh completion for juju -*- shell-script -*-
|
||||
|
||||
__juju_debug()
|
||||
{
|
||||
local file="$BASH_COMP_DEBUG_FILE"
|
||||
if [[ -n ${file} ]]; then
|
||||
echo "$*" >> "${file}"
|
||||
fi
|
||||
}
|
||||
|
||||
__juju_help_options()
|
||||
{
|
||||
local out line token cleaned desc f
|
||||
local -a opts pending
|
||||
typeset -U opts
|
||||
|
||||
__juju_debug "[options] called with args: $*"
|
||||
out=$(command juju help "$@" 2>/dev/null)
|
||||
local rc=$?
|
||||
__juju_debug "[options] juju help exit code: $rc, output length: ${#out}"
|
||||
(( rc )) && return 1
|
||||
|
||||
while IFS= read -r line; do
|
||||
if [[ "$line" =~ '^[[:space:]]{0,3}-' ]]; then
|
||||
for f in "${pending[@]}"; do opts+=("$f"); done
|
||||
pending=()
|
||||
for token in ${(z)line}; do
|
||||
cleaned="${token%%,*}"
|
||||
cleaned="${cleaned%%;*}"
|
||||
cleaned="${cleaned%%]*}"
|
||||
cleaned="${cleaned%%)*}"
|
||||
cleaned="${cleaned%%=<*}"
|
||||
cleaned="${cleaned%%=*}"
|
||||
cleaned="${cleaned%%<*}"
|
||||
cleaned="${cleaned%%\[*}"
|
||||
cleaned="${cleaned%%\(*}"
|
||||
[[ "$cleaned" == --* || "$cleaned" == -[[:alnum:]] ]] || continue
|
||||
[[ "$cleaned" == "-" || "$cleaned" == "--" ]] && continue
|
||||
__juju_debug "[options] found flag: $cleaned"
|
||||
pending+=("$cleaned")
|
||||
done
|
||||
elif (( ${#pending} )) && [[ -n "$line" ]]; then
|
||||
desc="${line#"${line%%[![:space:]]*}"}"
|
||||
desc="${desc//:/\\:}"
|
||||
__juju_debug "[options] desc for ${pending[*]}: $desc"
|
||||
for f in "${pending[@]}"; do opts+=("${f}:${desc}"); done
|
||||
pending=()
|
||||
elif [[ -z "$line" ]]; then
|
||||
for f in "${pending[@]}"; do opts+=("$f"); done
|
||||
pending=()
|
||||
fi
|
||||
done < <(printf "%s\n" "$out")
|
||||
|
||||
for f in "${pending[@]}"; do opts+=("$f"); done
|
||||
__juju_debug "[options] total opts: ${#opts}, first few: ${opts[1]} ${opts[2]} ${opts[3]}"
|
||||
|
||||
printf "%s\n" "${opts[@]}"
|
||||
}
|
||||
|
||||
|
||||
__juju_help_commands()
|
||||
{
|
||||
local line cmd desc out
|
||||
out=$(command juju help commands 2>/dev/null) || return 1
|
||||
|
||||
while IFS= read -r line; do
|
||||
# Strip leading whitespace
|
||||
line="${line#"${line%%[![:space:]]*}"}"
|
||||
# Only process lines starting with an alphanumeric (command names)
|
||||
[[ "$line" =~ '^[[:alnum:]]' ]] || continue
|
||||
# Split on the first run of 2+ spaces: left = cmd, right = description
|
||||
cmd="${line%% *}"
|
||||
# Validate it's a clean command token (no spaces, only alnum and dash)
|
||||
[[ "$cmd" =~ '^[[:alnum:]][[:alnum:]-]*$' ]] || continue
|
||||
desc="${line#"$cmd"}"
|
||||
desc="${desc#"${desc%%[![:space:]]*}"}"
|
||||
if [[ -n "$desc" ]]; then
|
||||
printf "%s:%s\n" "$cmd" "$desc"
|
||||
else
|
||||
printf "%s\n" "$cmd"
|
||||
fi
|
||||
done <<< "$out"
|
||||
}
|
||||
|
||||
__juju_models()
|
||||
{
|
||||
# Optional argument: controller name. If given, fetch models for that controller.
|
||||
if [[ -n "$1" ]]; then
|
||||
command juju models -c "$1" --format=json 2>/dev/null \
|
||||
| command jq -r '.models[]."short-name"' 2>/dev/null
|
||||
else
|
||||
command juju models --format=json 2>/dev/null \
|
||||
| command jq -r '.models[]."short-name"' 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
# Complete a model token that may be prefixed with "controller:" — if a colon is
|
||||
# present, fetch models for that controller and offer "ctrl:model" completions.
|
||||
__juju_complete_model()
|
||||
{
|
||||
local current="$1"
|
||||
local -a completions
|
||||
|
||||
__juju_debug "[complete_model] current='${current}'"
|
||||
|
||||
if [[ "$current" == *:* ]]; then
|
||||
local ctrl="${current%%:*}"
|
||||
local models
|
||||
models=("${(@f)$(__juju_models "$ctrl")}")
|
||||
completions=("${models[@]/#/${ctrl}:}")
|
||||
__juju_debug "[complete_model] ctrl=${ctrl} completions=${#completions}: ${completions[*]}"
|
||||
compadd -S '' -q -- "${completions[@]}"
|
||||
else
|
||||
local -a models ctrls
|
||||
models=("${(@f)$(__juju_models)}")
|
||||
ctrls=("${(@f)$(__juju_controllers)}")
|
||||
__juju_debug "[complete_model] models=${#models}: ${models[*]}"
|
||||
__juju_debug "[complete_model] ctrls=${#ctrls}: ${ctrls[*]}"
|
||||
__juju_debug "[complete_model] calling _alternative"
|
||||
_alternative \
|
||||
'models:models:{__juju_debug "[complete_model] compadd models"; compadd "$expl[@]" -a models}' \
|
||||
'controllers:controllers:{__juju_debug "[complete_model] compadd ctrls"; compadd "$expl[@]" -S : -q -a ctrls}'
|
||||
__juju_debug "[complete_model] _alternative returned $?"
|
||||
fi
|
||||
}
|
||||
|
||||
# Commands whose first positional argument is a model name.
|
||||
_juju_model_commands=(
|
||||
destroy-model
|
||||
grant-model
|
||||
revoke-model
|
||||
switch
|
||||
)
|
||||
|
||||
# Flags that take a model name as their value.
|
||||
_juju_model_flags=(
|
||||
-m
|
||||
--model
|
||||
)
|
||||
|
||||
__juju_controllers()
|
||||
{
|
||||
command juju controllers --format=json 2>/dev/null \
|
||||
| command jq -r '.controllers | keys | .[]' 2>/dev/null
|
||||
}
|
||||
|
||||
# Commands whose first positional argument is a controller name.
|
||||
_juju_controller_commands=(
|
||||
destroy-controller
|
||||
kill-controller
|
||||
login
|
||||
logout
|
||||
unregister
|
||||
)
|
||||
|
||||
# Flags that take a controller name as their value.
|
||||
_juju_controller_flags=(
|
||||
-c
|
||||
--controller
|
||||
)
|
||||
|
||||
_juju()
|
||||
{
|
||||
__juju_debug "[_juju] curcontext: ${curcontext}"
|
||||
local -a completions
|
||||
|
||||
__juju_debug "[_juju] words: ${words[*]}, CURRENT: $CURRENT"
|
||||
|
||||
# Find the subcommand: first non-flag word typed after "juju", excluding the
|
||||
# word currently being completed (words[CURRENT]).
|
||||
local subcmd=""
|
||||
local i
|
||||
for (( i = 2; i < CURRENT; i++ )); do
|
||||
if [[ "${words[i]}" != -* ]]; then
|
||||
subcmd="${words[i]}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
local current="${words[CURRENT]}"
|
||||
local prev="${words[CURRENT-1]}"
|
||||
|
||||
__juju_debug "[_juju] subcmd: '${subcmd}', current: '${current}', prev: '${prev}'"
|
||||
|
||||
# Controller name completion: flag value (e.g. juju status -c <TAB>)
|
||||
if (( ${_juju_controller_flags[(I)$prev]} )); then
|
||||
completions=("${(@f)$(__juju_controllers)}")
|
||||
__juju_debug "[_juju] controller flag completions: ${#completions}"
|
||||
(( ${#completions} )) && _describe "controller" completions && return 0
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Model name completion: flag value (e.g. juju status -m <TAB> or -m ctrl:<TAB>)
|
||||
if (( ${_juju_model_flags[(I)$prev]} )); then
|
||||
__juju_debug "[_juju] model flag completion, current: '${current}'"
|
||||
__juju_complete_model "$current" && return 0
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ -z "$subcmd" ]]; then
|
||||
# No subcommand yet — complete subcommand names.
|
||||
completions=("${(@f)$(__juju_help_commands)}")
|
||||
__juju_debug "[_juju] command completions count: ${#completions}"
|
||||
(( ${#completions} )) && _describe "command" completions && return 0
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Controller name completion: positional arg (e.g. juju destroy-controller <TAB>)
|
||||
if (( ${_juju_controller_commands[(I)$subcmd]} )) && [[ "$current" != -* ]]; then
|
||||
completions=("${(@f)$(__juju_controllers)}")
|
||||
__juju_debug "[_juju] controller command completions: ${#completions}"
|
||||
(( ${#completions} )) && _describe "controller" completions && return 0
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Model name completion: positional arg (e.g. juju destroy-model <TAB> or ctrl:<TAB>)
|
||||
if (( ${_juju_model_commands[(I)$subcmd]} )) && [[ "$current" != -* ]]; then
|
||||
__juju_debug "[_juju] model command completion, current: '${current}'"
|
||||
__juju_complete_model "$current" && return 0
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Flag completion for all other subcommands (also shown without leading dash)
|
||||
completions=("${(@f)$(__juju_help_options "$subcmd")}")
|
||||
__juju_debug "[_juju] option completions count: ${#completions}"
|
||||
(( ${#completions} )) && _describe "option" completions && return 0
|
||||
return 1
|
||||
}
|
||||
@@ -1,17 +1,5 @@
|
||||
# ---------------------------------------------------------- #
|
||||
# Aliases and functions for juju (https://juju.is) #
|
||||
# ---------------------------------------------------------- #
|
||||
|
||||
# Load TAB completions
|
||||
# You need juju's bash completion script installed. By default bash-completion's
|
||||
# location will be used (i.e. pkg-config --variable=completionsdir bash-completion).
|
||||
completion_file="$(pkg-config --variable=completionsdir bash-completion 2>/dev/null)/juju" || \
|
||||
completion_file="/usr/share/bash-completion/completions/juju"
|
||||
[[ -f "$completion_file" ]] && source "$completion_file"
|
||||
unset completion_file
|
||||
|
||||
# ---------------------------------------------------------- #
|
||||
# Aliases (in alphabetic order) #
|
||||
# #
|
||||
# Generally, #
|
||||
# - `!` means --force --no-wait -y #
|
||||
@@ -132,6 +120,7 @@ jclean() {
|
||||
fi
|
||||
|
||||
echo
|
||||
local controller
|
||||
for controller in ${=controllers}; do
|
||||
timeout 2m juju destroy-controller --destroy-all-models --destroy-storage --force --no-wait -y $controller
|
||||
timeout 2m juju kill-controller -y -t 0 $controller 2>/dev/null
|
||||
@@ -165,10 +154,11 @@ jreld() {
|
||||
|
||||
# Return Juju current controller
|
||||
jcontroller() {
|
||||
local controller="$(awk '/current-controller/ {print $2}' ~/.local/share/juju/controllers.yaml)"
|
||||
if [[ -z "$controller" ]]; then
|
||||
return 1
|
||||
fi
|
||||
local file="${JUJU_DATA:-$HOME/.local/share/juju}/controllers.yaml"
|
||||
[[ -f "$file" ]] || return 1
|
||||
|
||||
local controller="$(awk '/current-controller/ {print $2}' "$file")"
|
||||
[[ -z "$controller" ]] && return 1
|
||||
|
||||
echo $controller
|
||||
return 0
|
||||
@@ -176,6 +166,9 @@ jcontroller() {
|
||||
|
||||
# Return Juju current model
|
||||
jmodel() {
|
||||
local file="${JUJU_DATA:-$HOME/.local/share/juju}/models.yaml"
|
||||
[[ -f "$file" ]] || return 1
|
||||
|
||||
local yqbin="$(whereis yq | awk '{print $2}')"
|
||||
|
||||
if [[ -z "$yqbin" ]]; then
|
||||
@@ -183,9 +176,10 @@ jmodel() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
local model="$(yq e ".controllers.$(jcontroller).current-model" < ~/.local/share/juju/models.yaml | cut -d/ -f2)"
|
||||
local controller="$(jcontroller)"
|
||||
local model="$(yq e ".controllers.[\"${controller}\"].current-model" < "${file}" | cut -d/ -f2)"
|
||||
|
||||
if [[ -z "$model" ]]; then
|
||||
if [[ -z "$model" || $model == "null" ]]; then
|
||||
echo "--"
|
||||
return 1
|
||||
fi
|
||||
@@ -194,9 +188,10 @@ jmodel() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# Watch juju status, with optional interval (default: 5 sec)
|
||||
# Watch juju status, with optional interval (default: 1 sec)
|
||||
wjst() {
|
||||
local interval="${1:-5}"
|
||||
command -v juju >/dev/null 2>&1 || return 1
|
||||
local interval="${1:-1}"
|
||||
shift $(( $# > 0 ))
|
||||
watch -n "$interval" --color juju status --relations --color "$@"
|
||||
}
|
||||
|
||||
@@ -4,37 +4,50 @@
|
||||
SHORT_HOST=${SHORT_HOST:-${(%):-%m}}
|
||||
|
||||
function {
|
||||
local agents
|
||||
local -a identities
|
||||
local -a options
|
||||
local _keychain_env_sh
|
||||
local _keychain_env_sh_gpg
|
||||
local -a identities
|
||||
local -a options
|
||||
local _keychain_env_sh
|
||||
local _keychain_env_sh_gpg
|
||||
local version_string major
|
||||
|
||||
# load agents to start.
|
||||
zstyle -s :omz:plugins:keychain agents agents
|
||||
# load identities to manage.
|
||||
zstyle -a :omz:plugins:keychain identities identities
|
||||
|
||||
# load identities to manage.
|
||||
zstyle -a :omz:plugins:keychain identities identities
|
||||
# load additional options
|
||||
zstyle -a :omz:plugins:keychain options options
|
||||
|
||||
# load additional options
|
||||
zstyle -a :omz:plugins:keychain options options
|
||||
# Detect major version robustly: case-insensitive, tolerant of
|
||||
# "keychain 3.0.0_beta3" / "Keychain 3.0.0" / etc.
|
||||
version_string=$(keychain --version 2>&1)
|
||||
if [[ "${version_string:l}" =~ '([0-9]+)\.[0-9]+' ]]; then
|
||||
major=${match[1]}
|
||||
else
|
||||
major=2
|
||||
fi
|
||||
|
||||
# Check keychain version to decide whether to use --agents
|
||||
local version_string=$(keychain --version 2>&1)
|
||||
# start keychain, only use --agents for versions below 2.9.0
|
||||
autoload -Uz is-at-least
|
||||
if [[ "$version_string" =~ 'keychain ([0-9]+\.[0-9]+)' ]] && \
|
||||
is-at-least 2.9 "$match[1]"; then
|
||||
keychain ${^options:-} ${^identities} --host $SHORT_HOST
|
||||
else
|
||||
keychain ${^options:-} --agents ${agents:-gpg} ${^identities} --host $SHORT_HOST
|
||||
fi
|
||||
if (( major >= 3 )); then
|
||||
# Keychain 3.x: subcommand-based CLI, no --agents.
|
||||
keychain add ${^options:-} --host $SHORT_HOST ${^identities}
|
||||
else
|
||||
local agents
|
||||
|
||||
# Get the filenames to store/lookup the environment from
|
||||
_keychain_env_sh="$HOME/.keychain/$SHORT_HOST-sh"
|
||||
_keychain_env_sh_gpg="$HOME/.keychain/$SHORT_HOST-sh-gpg"
|
||||
# load agents to start (only used by keychain < 2.9).
|
||||
zstyle -s :omz:plugins:keychain agents agents
|
||||
|
||||
# Source environment settings.
|
||||
[ -f "$_keychain_env_sh" ] && . "$_keychain_env_sh"
|
||||
[ -f "$_keychain_env_sh_gpg" ] && . "$_keychain_env_sh_gpg"
|
||||
autoload -Uz is-at-least
|
||||
if [[ "${version_string:l}" =~ 'keychain ([0-9]+\.[0-9]+)' ]] && \
|
||||
is-at-least 2.9 "$match[1]"; then
|
||||
keychain ${^options:-} ${^identities} --host $SHORT_HOST
|
||||
else
|
||||
keychain ${^options:-} --agents ${agents:-gpg} ${^identities} --host $SHORT_HOST
|
||||
fi
|
||||
fi
|
||||
|
||||
# Get the filenames to store/lookup the environment from
|
||||
_keychain_env_sh="$HOME/.keychain/$SHORT_HOST-sh"
|
||||
_keychain_env_sh_gpg="$HOME/.keychain/$SHORT_HOST-sh-gpg"
|
||||
|
||||
# Source environment settings.
|
||||
[ -f "$_keychain_env_sh" ] && . "$_keychain_env_sh"
|
||||
[ -f "$_keychain_env_sh_gpg" ] && . "$_keychain_env_sh_gpg"
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
# kube-ps1: Kubernetes prompt for bash and zsh
|
||||
# kube-ps1: Kubernetes prompt for bash, zsh, and fish
|
||||
|
||||

|
||||
[](https://github.com/jonmosco/kube-ps1/actions/workflows/ci.yml)
|
||||
|
||||
A script that lets you add the current Kubernetes context and namespace
|
||||
configured on `kubectl` to your Bash/Zsh prompt strings (i.e. the `$PS1`).
|
||||
A script that lets you add the current Kubernetes context and namespace configured on `kubectl` to your Bash, Zsh, or Fish prompt.
|
||||
|
||||
Inspired by several tools used to simplify usage of `kubectl`.
|
||||
|
||||
@@ -77,6 +76,21 @@ source /path/to/kube-ps1.sh
|
||||
PS1='[\u@\h \W $(kube_ps1)]\$ '
|
||||
```
|
||||
|
||||
#### Fish
|
||||
|
||||
Add this to `~/.config/fish/config.fish`:
|
||||
|
||||
```fish
|
||||
source /path/to/kube-ps1.fish
|
||||
|
||||
function fish_prompt
|
||||
echo -n (kube_ps1) ' '
|
||||
# your existing prompt here
|
||||
end
|
||||
```
|
||||
|
||||
> Note: Fish users should source `kube-ps1.fish` instead of `kube-ps1.sh`.
|
||||
|
||||
## Requirements
|
||||
|
||||
The default prompt assumes you have the `kubectl` command line utility installed.
|
||||
@@ -184,8 +198,7 @@ If the font is not properly installed, and the glyph is not available, it will d
|
||||
|
||||
## Customization
|
||||
|
||||
The default settings can be overridden in `~/.bashrc` or `~/.zshrc` by setting
|
||||
the following variables:
|
||||
The default settings can be overridden in `~/.bashrc`, `~/.zshrc`, or `~/.config/fish/config.fish` by setting the following variables:
|
||||
|
||||
| Variable | Default | Meaning |
|
||||
| :------- | :-----: | ------- |
|
||||
|
||||
@@ -49,7 +49,7 @@ _kube_ps1_shell_type() {
|
||||
elif [ "${BASH_VERSION-}" ]; then
|
||||
_KUBE_PS1_SHELL_TYPE="bash"
|
||||
fi
|
||||
echo $_KUBE_PS1_SHELL_TYPE
|
||||
echo "$_KUBE_PS1_SHELL_TYPE"
|
||||
}
|
||||
|
||||
_kube_ps1_init() {
|
||||
@@ -65,6 +65,15 @@ _kube_ps1_init() {
|
||||
_KUBE_PS1_TPUT_AVAILABLE=false
|
||||
fi
|
||||
|
||||
# Detect stat type once (not needed for zsh which uses zstat builtin)
|
||||
if [[ "${_KUBE_PS1_SHELL}" != "zsh" ]]; then
|
||||
if stat -c "%s" /dev/null &> /dev/null; then
|
||||
_KUBE_PS1_STAT_TYPE="gnu"
|
||||
else
|
||||
_KUBE_PS1_STAT_TYPE="bsd"
|
||||
fi
|
||||
fi
|
||||
|
||||
case "${_KUBE_PS1_SHELL}" in
|
||||
"zsh")
|
||||
_KUBE_PS1_OPEN_ESC="%{"
|
||||
@@ -98,8 +107,8 @@ _kube_ps1_color_fg() {
|
||||
magenta) _KUBE_PS1_FG_CODE=5;;
|
||||
cyan) _KUBE_PS1_FG_CODE=6;;
|
||||
white) _KUBE_PS1_FG_CODE=7;;
|
||||
# 256
|
||||
[0-9]|[1-9][0-9]|[1][0-9][0-9]|[2][0-4][0-9]|[2][5][0-6]) _KUBE_PS1_FG_CODE="${1}";;
|
||||
# 256 colors
|
||||
[0-9]|[1-9][0-9]|[1][0-9][0-9]|[2][0-4][0-9]|[2][5][0-5]) _KUBE_PS1_FG_CODE="${1}";;
|
||||
*) _KUBE_PS1_FG_CODE=default
|
||||
esac
|
||||
|
||||
@@ -111,7 +120,7 @@ _kube_ps1_color_fg() {
|
||||
elif [[ "${_KUBE_PS1_SHELL}" == "bash" ]]; then
|
||||
if [[ "${_KUBE_PS1_TPUT_AVAILABLE}" == "true" ]]; then
|
||||
_KUBE_PS1_FG_CODE="$(tput setaf "${_KUBE_PS1_FG_CODE}")"
|
||||
elif [[ $_KUBE_PS1_FG_CODE -ge 0 ]] && [[ $_KUBE_PS1_FG_CODE -le 256 ]]; then
|
||||
elif [[ $_KUBE_PS1_FG_CODE -ge 0 ]] && [[ $_KUBE_PS1_FG_CODE -le 255 ]]; then
|
||||
_KUBE_PS1_FG_CODE="\033[38;5;${_KUBE_PS1_FG_CODE}m"
|
||||
else
|
||||
_KUBE_PS1_FG_CODE="${_KUBE_PS1_DEFAULT_FG}"
|
||||
@@ -131,20 +140,20 @@ _kube_ps1_color_bg() {
|
||||
magenta) _KUBE_PS1_BG_CODE=5;;
|
||||
cyan) _KUBE_PS1_BG_CODE=6;;
|
||||
white) _KUBE_PS1_BG_CODE=7;;
|
||||
# 256
|
||||
[0-9]|[1-9][0-9]|[1][0-9][0-9]|[2][0-4][0-9]|[2][5][0-6]) _KUBE_PS1_BG_CODE="${1}";;
|
||||
*) _KUBE_PS1_BG_CODE=$'\033[0m';;
|
||||
# 256 colors
|
||||
[0-9]|[1-9][0-9]|[1][0-9][0-9]|[2][0-4][0-9]|[2][5][0-5]) _KUBE_PS1_BG_CODE="${1}";;
|
||||
*) _KUBE_PS1_BG_CODE=default
|
||||
esac
|
||||
|
||||
if [[ "${_KUBE_PS1_BG_CODE}" == "default" ]]; then
|
||||
_KUBE_PS1_FG_CODE="${_KUBE_PS1_DEFAULT_BG}"
|
||||
_KUBE_PS1_BG_CODE="${_KUBE_PS1_DEFAULT_BG}"
|
||||
return
|
||||
elif [[ "${_KUBE_PS1_SHELL}" == "zsh" ]]; then
|
||||
_KUBE_PS1_BG_CODE="%K{$_KUBE_PS1_BG_CODE}"
|
||||
elif [[ "${_KUBE_PS1_SHELL}" == "bash" ]]; then
|
||||
if [[ "${_KUBE_PS1_TPUT_AVAILABLE}" == "true" ]]; then
|
||||
_KUBE_PS1_BG_CODE="$(tput setab "${_KUBE_PS1_BG_CODE}")"
|
||||
elif [[ $_KUBE_PS1_BG_CODE -ge 0 ]] && [[ $_KUBE_PS1_BG_CODE -le 256 ]]; then
|
||||
elif [[ $_KUBE_PS1_BG_CODE -ge 0 ]] && [[ $_KUBE_PS1_BG_CODE -le 255 ]]; then
|
||||
_KUBE_PS1_BG_CODE="\033[48;5;${_KUBE_PS1_BG_CODE}m"
|
||||
else
|
||||
_KUBE_PS1_BG_CODE="${_KUBE_PS1_DEFAULT_BG}"
|
||||
@@ -154,7 +163,7 @@ _kube_ps1_color_bg() {
|
||||
}
|
||||
|
||||
_kube_ps1_binary_check() {
|
||||
command -v $1 >/dev/null
|
||||
command -v "$1" >/dev/null
|
||||
}
|
||||
|
||||
_kube_ps1_symbol() {
|
||||
@@ -171,6 +180,7 @@ _kube_ps1_symbol() {
|
||||
local oc_glyph=$'\ue7b7'
|
||||
local oc_symbol_color=red
|
||||
local custom_symbol_color="${KUBE_PS1_SYMBOL_COLOR:-$k8s_symbol_color}"
|
||||
local KUBE_PS1_RESET_COLOR="${_KUBE_PS1_OPEN_ESC}${_KUBE_PS1_DEFAULT_FG}${_KUBE_PS1_CLOSE_ESC}"
|
||||
|
||||
# Choose the symbol based on the provided argument or environment variable
|
||||
case "${symbol_arg}" in
|
||||
@@ -225,15 +235,27 @@ _kube_ps1_file_newer_than() {
|
||||
if [[ "${_KUBE_PS1_SHELL}" == "zsh" ]]; then
|
||||
# Use zstat '-F %s.%s' to make it compatible with low zsh version (eg: 5.0.2)
|
||||
mtime=$(zstat -L +mtime -F %s.%s "${file}")
|
||||
elif stat -c "%s" /dev/null &> /dev/null; then
|
||||
# GNU stat
|
||||
elif [[ "${_KUBE_PS1_STAT_TYPE}" == "gnu" ]]; then
|
||||
mtime=$(stat -L -c %Y "${file}")
|
||||
else
|
||||
# BSD stat
|
||||
mtime=$(stat -L -f %m "$file")
|
||||
fi
|
||||
|
||||
[[ "${mtime}" -gt "${check_time}" ]]
|
||||
[[ "${mtime}" -gt "${check_time}" ]] && return 0
|
||||
|
||||
# If the path is a symlink, also check the symlink's own mtime
|
||||
if [[ -L "${file}" ]]; then
|
||||
if [[ "${_KUBE_PS1_SHELL}" == "zsh" ]]; then
|
||||
mtime=$(zstat +mtime -F %s.%s "${file}")
|
||||
elif [[ "${_KUBE_PS1_STAT_TYPE}" == "gnu" ]]; then
|
||||
mtime=$(stat -c %Y "${file}")
|
||||
else
|
||||
mtime=$(stat -f %m "$file")
|
||||
fi
|
||||
[[ "${mtime}" -gt "${check_time}" ]] && return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
_kube_ps1_prompt_update() {
|
||||
@@ -280,7 +302,6 @@ _kube_ps1_prompt_update() {
|
||||
_kube_ps1_get_context() {
|
||||
if [[ "${KUBE_PS1_CONTEXT_ENABLE}" == true ]]; then
|
||||
KUBE_PS1_CONTEXT="$(${KUBE_PS1_BINARY} config current-context 2>/dev/null)"
|
||||
# Set namespace to 'N/A' if it is not defined
|
||||
KUBE_PS1_CONTEXT="${KUBE_PS1_CONTEXT:-N/A}"
|
||||
|
||||
if [[ -n "${KUBE_PS1_CLUSTER_FUNCTION}" ]]; then
|
||||
@@ -303,7 +324,7 @@ _kube_ps1_get_ns() {
|
||||
_kube_ps1_get_context_ns() {
|
||||
# Set the command time
|
||||
if [[ "${_KUBE_PS1_SHELL}" == "bash" ]]; then
|
||||
if ((BASH_VERSINFO[0] >= 4 && BASH_VERSINFO[1] >= 2)); then
|
||||
if ((BASH_VERSINFO[0] > 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] >= 2))); then
|
||||
_KUBE_PS1_LAST_TIME=$(printf '%(%s)T')
|
||||
else
|
||||
_KUBE_PS1_LAST_TIME=$(date +%s)
|
||||
@@ -312,9 +333,6 @@ _kube_ps1_get_context_ns() {
|
||||
_KUBE_PS1_LAST_TIME=$EPOCHREALTIME
|
||||
fi
|
||||
|
||||
KUBE_PS1_CONTEXT="${KUBE_PS1_CONTEXT:-N/A}"
|
||||
KUBE_PS1_NAMESPACE="${KUBE_PS1_NAMESPACE:-N/A}"
|
||||
|
||||
# Cache which cfgfiles we can read in case they change.
|
||||
local conf
|
||||
_KUBE_PS1_CFGFILES_READ_CACHE=
|
||||
@@ -358,12 +376,13 @@ EOF
|
||||
kubeon() {
|
||||
if [[ "${1}" == '-h' || "${1}" == '--help' ]]; then
|
||||
_kubeon_usage
|
||||
return 0
|
||||
elif [[ "${1}" == '-g' || "${1}" == '--global' ]]; then
|
||||
rm -f -- "${_KUBE_PS1_DISABLE_PATH}"
|
||||
elif [[ "$#" -ne 0 ]]; then
|
||||
echo -e "error: unrecognized flag ${1}\\n"
|
||||
echo -e "error: unrecognized flag ${1}\\n" >&2
|
||||
_kubeon_usage
|
||||
return
|
||||
return 1
|
||||
fi
|
||||
|
||||
KUBE_PS1_ENABLED=on
|
||||
@@ -372,13 +391,14 @@ kubeon() {
|
||||
kubeoff() {
|
||||
if [[ "${1}" == '-h' || "${1}" == '--help' ]]; then
|
||||
_kubeoff_usage
|
||||
return 0
|
||||
elif [[ "${1}" == '-g' || "${1}" == '--global' ]]; then
|
||||
mkdir -p -- "$(dirname "${_KUBE_PS1_DISABLE_PATH}")"
|
||||
touch -- "${_KUBE_PS1_DISABLE_PATH}"
|
||||
elif [[ $# -ne 0 ]]; then
|
||||
echo "error: unrecognized flag ${1}" >&2
|
||||
echo -e "error: unrecognized flag ${1}\\n" >&2
|
||||
_kubeoff_usage
|
||||
return
|
||||
return 1
|
||||
fi
|
||||
|
||||
KUBE_PS1_ENABLED=off
|
||||
@@ -390,15 +410,9 @@ kube_ps1() {
|
||||
[[ -z "${KUBE_PS1_CONTEXT}" ]] && [[ "${KUBE_PS1_CONTEXT_ENABLE}" == true ]] && return
|
||||
[[ "${KUBE_PS1_CONTEXT}" == "N/A" ]] && [[ ${KUBE_PS1_HIDE_IF_NOCONTEXT} == true ]] && return
|
||||
|
||||
|
||||
local KUBE_PS1
|
||||
local KUBE_PS1_RESET_COLOR="${_KUBE_PS1_OPEN_ESC}${_KUBE_PS1_DEFAULT_FG}${_KUBE_PS1_CLOSE_ESC}"
|
||||
|
||||
# If background color is set, reset color should also reset the background
|
||||
# if [[ -n "${KUBE_PS1_BG_COLOR}" ]]; then
|
||||
# KUBE_PS1_RESET_COLOR="${_KUBE_PS1_OPEN_ESC}${_KUBE_PS1_DEFAULT_FG}${_KUBE_PS1_DEFAULT_BG}${_KUBE_PS1_CLOSE_ESC}"
|
||||
# fi
|
||||
|
||||
# Background Color
|
||||
[[ -n "${KUBE_PS1_BG_COLOR}" ]] && KUBE_PS1+="$(_kube_ps1_color_bg "${KUBE_PS1_BG_COLOR}")"
|
||||
|
||||
|
||||
@@ -22,6 +22,21 @@ RPS1='$(kubectx_prompt_info)'
|
||||
PROMPT="$PROMPT"'$(kubectx_prompt_info)'
|
||||
```
|
||||
|
||||
The context is loaded asynchronously on supported versions of zsh so that
|
||||
`kubectl` does not block the prompt. To restore synchronous behavior, add this
|
||||
before Oh My Zsh is sourced:
|
||||
|
||||
```zsh
|
||||
zstyle ':omz:alpha:plugins:kubectx' async-prompt no
|
||||
```
|
||||
|
||||
If your theme calls `kubectx_prompt_info` indirectly through another function,
|
||||
force registration of the async handler instead:
|
||||
|
||||
```zsh
|
||||
zstyle ':omz:alpha:plugins:kubectx' async-prompt force
|
||||
```
|
||||
|
||||
### Custom context names
|
||||
|
||||
You can rename the default context name for better readability or additional formatting.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
typeset -g -A kubectx_mapping
|
||||
|
||||
function kubectx_prompt_info() {
|
||||
function _omz_kubectx_prompt_info() {
|
||||
(( $+commands[kubectl] )) || return
|
||||
|
||||
local current_ctx=$(kubectl config current-context 2> /dev/null)
|
||||
@@ -13,3 +13,34 @@ function kubectx_prompt_info() {
|
||||
# the context name, as it could contain a % character.
|
||||
echo "${kubectx_mapping[$current_ctx]:-${current_ctx:gs/%/%%}}"
|
||||
}
|
||||
|
||||
function kubectx_prompt_info() {
|
||||
if (( ${+_OMZ_ASYNC_OUTPUT} )) \
|
||||
&& [[ -n "${_OMZ_ASYNC_OUTPUT[_omz_kubectx_prompt_info]-}" ]]; then
|
||||
echo -n "${_OMZ_ASYNC_OUTPUT[_omz_kubectx_prompt_info]}"
|
||||
fi
|
||||
}
|
||||
|
||||
local _style
|
||||
if zstyle -t ':omz:alpha:plugins:kubectx' async-prompt \
|
||||
|| { is-at-least 5.0.6 && zstyle -T ':omz:alpha:plugins:kubectx' async-prompt }; then
|
||||
function _defer_async_kubectx_register() {
|
||||
case "${PS1}:${PS2}:${PS3}:${PS4}:${RPROMPT-}:${RPS1-}:${RPS2-}:${RPS3-}:${RPS4-}" in
|
||||
*(\$\(kubectx_prompt_info\)|\`kubectx_prompt_info\`)*)
|
||||
_omz_register_handler _omz_kubectx_prompt_info
|
||||
;;
|
||||
esac
|
||||
|
||||
add-zsh-hook -d precmd _defer_async_kubectx_register
|
||||
unset -f _defer_async_kubectx_register
|
||||
}
|
||||
|
||||
autoload -Uz add-zsh-hook
|
||||
precmd_functions=(_defer_async_kubectx_register $precmd_functions)
|
||||
elif zstyle -s ':omz:alpha:plugins:kubectx' async-prompt _style && [[ $_style == "force" ]]; then
|
||||
_omz_register_handler _omz_kubectx_prompt_info
|
||||
else
|
||||
function kubectx_prompt_info() {
|
||||
_omz_kubectx_prompt_info
|
||||
}
|
||||
fi
|
||||
|
||||
@@ -10,6 +10,7 @@ plugins=(... laravel)
|
||||
|:-:|:-:|
|
||||
| `artisan` | `php artisan` |
|
||||
| `pas` | `php artisan serve` |
|
||||
| `pad` | `php artisan dev` |
|
||||
| `pats` | `php artisan test` |
|
||||
|
||||
## Database
|
||||
|
||||
@@ -4,6 +4,7 @@ alias bob='php artisan bob::build'
|
||||
|
||||
# Development
|
||||
alias pas='php artisan serve'
|
||||
alias pad='php artisan dev'
|
||||
alias pats='php artisan test'
|
||||
|
||||
# Database
|
||||
|
||||
@@ -8,7 +8,7 @@ alias puo="sudo port upgrade outdated"
|
||||
alias pup="sudo port selfupdate && sudo port upgrade outdated"
|
||||
|
||||
port-livecheck-maintainer() {
|
||||
(( ${+commands[port]} == 0 )) || {
|
||||
(( ${+commands[port]} )) || {
|
||||
print -- "port: not found" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Proton Pass CLI plugin
|
||||
|
||||
This plugin adds completions for [Proton Pass CLI](https://protonpass.github.io/pass-cli/).
|
||||
|
||||
To use it, add `pass-cli` to the plugins array in your zshrc file:
|
||||
|
||||
```zsh
|
||||
plugins=(... pass-cli)
|
||||
```
|
||||
|
||||
This plugin does not add any aliases.
|
||||
@@ -0,0 +1,14 @@
|
||||
# Autocompletion for Proton Pass CLI (pass-cli)
|
||||
if (( ! $+commands[pass-cli] )); then
|
||||
return
|
||||
fi
|
||||
|
||||
# If the completion file doesn't exist yet, we need to autoload it and
|
||||
# bind it to `pass-cli`. Otherwise, compinit will have already done that.
|
||||
if [[ ! -f "$ZSH_CACHE_DIR/completions/_pass-cli" ]]; then
|
||||
typeset -g -A _comps
|
||||
autoload -Uz _pass-cli
|
||||
_comps[pass-cli]=_pass-cli
|
||||
fi
|
||||
|
||||
pass-cli completions zsh >| "$ZSH_CACHE_DIR/completions/_pass-cli" &|
|
||||
@@ -91,13 +91,14 @@ bindkey -M vicmd $PER_DIRECTORY_HISTORY_TOGGLE per-directory-history-toggle-hist
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
_per_directory_history_directory="$HISTORY_BASE${PWD:A}/history"
|
||||
_per_directory_history_global="$HISTFILE"
|
||||
|
||||
function _per-directory-history-change-directory() {
|
||||
_per_directory_history_directory="$HISTORY_BASE${PWD:A}/history"
|
||||
mkdir -p ${_per_directory_history_directory:h}
|
||||
if [[ $_per_directory_history_is_global == false ]]; then
|
||||
#save to the global history
|
||||
fc -AI $HISTFILE
|
||||
fc -AI "$_per_directory_history_global"
|
||||
#save history to previous file
|
||||
local prev="$HISTORY_BASE${OLDPWD:A}/history"
|
||||
mkdir -p ${prev:h}
|
||||
@@ -125,10 +126,9 @@ function _per-directory-history-addhistory() {
|
||||
if [[ -o share_history ]] || \
|
||||
[[ -o inc_append_history ]] || \
|
||||
[[ -o inc_append_history_time ]]; then
|
||||
fc -AI $HISTFILE
|
||||
fc -AI "$_per_directory_history_global"
|
||||
fc -AI $_per_directory_history_directory
|
||||
fi
|
||||
fc -p $_per_directory_history_directory
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -147,12 +147,13 @@ function _per-directory-history-precmd() {
|
||||
}
|
||||
|
||||
function _per-directory-history-set-directory-history() {
|
||||
fc -AI $HISTFILE
|
||||
fc -AI "$_per_directory_history_global"
|
||||
local original_histsize=$HISTSIZE
|
||||
HISTSIZE=0
|
||||
HISTSIZE=$original_histsize
|
||||
if [[ -e "$_per_directory_history_directory" ]]; then
|
||||
fc -R "$_per_directory_history_directory"
|
||||
fc -p "$_per_directory_history_directory"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -161,8 +162,9 @@ function _per-directory-history-set-global-history() {
|
||||
local original_histsize=$HISTSIZE
|
||||
HISTSIZE=0
|
||||
HISTSIZE=$original_histsize
|
||||
if [[ -e "$HISTFILE" ]]; then
|
||||
fc -R "$HISTFILE"
|
||||
if [[ -e "$_per_directory_history_global" ]]; then
|
||||
fc -R "$_per_directory_history_global"
|
||||
fc -p "$_per_directory_history_global"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ plugins=(... pipenv ...)
|
||||
|
||||
## Features
|
||||
|
||||
- Adds completion for pipenv
|
||||
- Adds completion for pipenv ([install the `argcomplete` package to get it working with pipenv >= 2026.5.0](https://pipenv.pypa.io/en/latest/shell.html#shell-completion))
|
||||
- Auto activates and deactivates pipenv shell
|
||||
- Adds short aliases for common pipenv commands
|
||||
- `pch` is aliased to `pipenv check`
|
||||
@@ -29,6 +29,18 @@ plugins=(... pipenv ...)
|
||||
- `pvenv` is aliased to `pipenv --venv`
|
||||
- `ppy` is aliased to `pipenv --py`
|
||||
|
||||
## Cache
|
||||
|
||||
This plugin caches the Pipenv version to avoid running `pipenv --version` on every shell startup. The cache is automatically refreshed asynchronously when the plugin is loaded, which is usually when you start a new terminal session.
|
||||
|
||||
For legacy Pipenv versions, the generated completion script is also cached.
|
||||
|
||||
The cache is stored at:
|
||||
|
||||
- `$ZSH_CACHE_DIR/pipenv_version` version of Pipenv, used to choose between argcomplete-based completion and legacy Click-based completion.
|
||||
|
||||
- `$ZSH_CACHE_DIR/completions/_pipenv` legacy Click-based completion script.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Shell activation
|
||||
|
||||
@@ -2,15 +2,49 @@ if (( ! $+commands[pipenv] )); then
|
||||
return
|
||||
fi
|
||||
|
||||
# If the completion file doesn't exist yet, we need to autoload it and
|
||||
# bind it to `pipenv`. Otherwise, compinit will have already done that.
|
||||
if [[ ! -f "$ZSH_CACHE_DIR/completions/_pipenv" ]]; then
|
||||
typeset -g -A _comps
|
||||
autoload -Uz _pipenv
|
||||
_comps[pipenv]=_pipenv
|
||||
# Compatibility note:
|
||||
# pipenv < 2026.5.0 used Click-based shell completion driven by the
|
||||
# _PIPENV_COMPLETE environment variable.
|
||||
#
|
||||
# pipenv >= 2026.5.0 removed this mechanism and switched to argcomplete-based
|
||||
# completion using register-python-argcomplete instead.
|
||||
|
||||
autoload -Uz is-at-least
|
||||
|
||||
_pipenv_version_cache="$ZSH_CACHE_DIR/pipenv_version"
|
||||
|
||||
if [[ -f "$_pipenv_version_cache" ]]; then
|
||||
_pipenv_version="$(< "$_pipenv_version_cache" 2>/dev/null)"
|
||||
else
|
||||
_pipenv_version="${$(pipenv --version 2>/dev/null)#pipenv, version }"
|
||||
fi
|
||||
|
||||
_PIPENV_COMPLETE=zsh_source pipenv >| "$ZSH_CACHE_DIR/completions/_pipenv" &|
|
||||
{
|
||||
_pipenv_fresh_version="${$(pipenv --version 2>/dev/null)#pipenv, version }"
|
||||
[[ -n "$_pipenv_fresh_version" ]] && print -r -- "$_pipenv_fresh_version" >| "$_pipenv_version_cache"
|
||||
} &|
|
||||
|
||||
if is-at-least 2026.5.0 "$_pipenv_version"; then
|
||||
# pipenv >= 2026.5.0: argcomplete-based completion (no legacy fallback)
|
||||
if (( $+commands[register-python-argcomplete] )); then
|
||||
autoload -Uz bashcompinit
|
||||
bashcompinit
|
||||
|
||||
eval "$(register-python-argcomplete pipenv)"
|
||||
fi
|
||||
else
|
||||
# legacy Click-based completion via _PIPENV_COMPLETE
|
||||
|
||||
# If the completion file doesn't exist yet, we need to autoload it and
|
||||
# bind it to `pipenv`. Otherwise, compinit will have already done that.
|
||||
if [[ ! -f "$ZSH_CACHE_DIR/completions/_pipenv" ]]; then
|
||||
typeset -g -A _comps
|
||||
autoload -Uz _pipenv
|
||||
_comps[pipenv]=_pipenv
|
||||
fi
|
||||
|
||||
_PIPENV_COMPLETE=zsh_source pipenv >| "$ZSH_CACHE_DIR/completions/_pipenv" &|
|
||||
fi
|
||||
|
||||
if zstyle -T ':omz:plugins:pipenv' auto-shell; then
|
||||
# Automatic pipenv shell activation/deactivation
|
||||
@@ -34,6 +68,7 @@ if zstyle -T ':omz:plugins:pipenv' auto-shell; then
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
autoload -U add-zsh-hook
|
||||
add-zsh-hook chpwd _togglePipenvShell
|
||||
_togglePipenvShell
|
||||
|
||||
+1
-1
@@ -314,7 +314,7 @@ _pm2_subcommands() {
|
||||
(( $+functions[_pm2_id_names] )) ||
|
||||
_pm2_id_names() {
|
||||
local app_list=$(pm2 list -m)
|
||||
local -a names=(${(@f)"$(echo $app_list | awk '/^\+---/{sub("+--- ", ""); print}')"})
|
||||
local -a names=(${(@f)"$(echo $app_list | awk '/^\+---/{sub("\\+--- ", ""); print}')"})
|
||||
local -a ids=(${(@f)"$(echo $app_list | awk '/^pm2 id/{sub("pm2 id :", ""); print}')"})
|
||||
|
||||
if (( ${#ids} > 0 )); then
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Pulumi
|
||||
|
||||
This is an **Oh My Zsh plugin** for the [**Pulumi CLI**](https://www.pulumi.com/docs/iac/cli/),
|
||||
an Infrastructure as Code (IaC) tool for building, deploying and managing cloud infrastucture.
|
||||
an Infrastructure as Code (IaC) tool for building, deploying and managing cloud infrastructure.
|
||||
|
||||
This plugin provides:
|
||||
|
||||
|
||||
@@ -24,11 +24,12 @@ plugins=(... rails)
|
||||
| `rdm` | `rails db:migrate` | Run pending db migrations |
|
||||
| `rdmd` | `rails db:migrate:down` | Undo specific db migration |
|
||||
| `rdmr` | `rails db:migrate:redo` | Redo specific db migration |
|
||||
| `rdmrs` | `rails db:migrate:reset` | Delete the database and set it up again from scratch. |
|
||||
| `rdms` | `rails db:migrate:status` | Show current db migration status |
|
||||
| `rdmtc` | `rails db:migrate db:test:clone` | Run pending migrations and clone db into test database |
|
||||
| `rdmu` | `rails db:migrate:up` | Run specific db migration |
|
||||
| `rdr` | `rails db:rollback` | Roll back the last migration |
|
||||
| `rdrs` | `rails db:reset` | Delete the database and set it up again |
|
||||
| `rdrs` | `rails db:reset` | Delete the database and set it up again from schema. |
|
||||
| `rds` | `rails db:seed` | Seed the database |
|
||||
| `rdsl` | `rails db:schema:load` | Load the database schema |
|
||||
| `rdtc` | `rails db:test:clone` | Clone the database into the test database |
|
||||
|
||||
@@ -214,6 +214,7 @@ _rails_subcommands() {
|
||||
"db\:migrate[Migrate the database]"
|
||||
"db\:migrate\:down[Run the 'down' for a given migration VERSION]"
|
||||
"db\:migrate\:redo[Roll back the database one migration and re-migrate up]"
|
||||
"db\:migrate\:reset[Drop and recreate all databases from scratch for the current environment]"
|
||||
"db\:migrate\:status[Display status of migrations]"
|
||||
"db\:migrate\:up[Run the 'up' for a given migration VERSION]"
|
||||
"db\:prepare[Run setup if database does not exist, or run migrations if it does]"
|
||||
|
||||
@@ -52,6 +52,7 @@ alias rdd='rails db:drop'
|
||||
alias rdm='rails db:migrate'
|
||||
alias rdmd='rails db:migrate:down'
|
||||
alias rdmr='rails db:migrate:redo'
|
||||
alias rdmrs='rails db:migrate:reset'
|
||||
alias rdms='rails db:migrate:status'
|
||||
alias rdmtc='rails db:migrate db:test:clone'
|
||||
alias rdmu='rails db:migrate:up'
|
||||
|
||||
@@ -167,7 +167,7 @@ function _zsh_tmux_plugin_preexec()
|
||||
local -a tmux_cmd
|
||||
tmux_cmd=(command tmux)
|
||||
|
||||
eval $($tmux_cmd show-environment -s)
|
||||
eval "$($tmux_cmd show-environment -s)"
|
||||
}
|
||||
|
||||
# Use the completions for tmux for our function
|
||||
@@ -183,7 +183,10 @@ function _tmux_directory_session() {
|
||||
# human friendly unique session name for this directory
|
||||
local session_name="${dir}-${md5:0:6}"
|
||||
# create or attach to the session
|
||||
tmux new -As "$session_name"
|
||||
local -a tmux_cmd
|
||||
tmux_cmd=(command tmux)
|
||||
[[ "$ZSH_TMUX_UNICODE" == "true" ]] && tmux_cmd+=(-u)
|
||||
$tmux_cmd new -As "$session_name"
|
||||
}
|
||||
|
||||
alias tds=_tmux_directory_session
|
||||
|
||||
@@ -16,3 +16,4 @@ Some of the commands include:
|
||||
* `deny <port>/<optional: protocol>` add deny rule
|
||||
* `disable` disables the firewall
|
||||
* `enable` enables the firewall
|
||||
* `route` add route rule
|
||||
|
||||
@@ -31,6 +31,7 @@ _1st_arguments=(
|
||||
'reject:add reject rule'
|
||||
'reload:reloads firewall'
|
||||
'reset:reset firewall'
|
||||
'route:add route rule'
|
||||
'show:show firewall report'
|
||||
'status:show firewall status'
|
||||
'version:display version information'
|
||||
@@ -43,6 +44,7 @@ _arguments -C \
|
||||
'1:: :->cmds' \
|
||||
'2:: :->subcmds' \
|
||||
'3:: :->subsubcmds' \
|
||||
'4:: :->subsubsubcmds' \
|
||||
&& return 0
|
||||
|
||||
local rules
|
||||
@@ -83,6 +85,17 @@ case "$state" in
|
||||
'raw' 'builtins' 'before-rules' 'user-rules' 'after-rules' 'logging-rules' 'listening' 'added' \
|
||||
&& ret=0
|
||||
;;
|
||||
(route)
|
||||
_values 'route' \
|
||||
'delete[delete route rule]' \
|
||||
'insert[insert route rule at NUM]' \
|
||||
'prepend[prepend route rule]' \
|
||||
'allow[add allow route rule]' \
|
||||
'deny[add deny route rule]' \
|
||||
'reject[add reject route rule]' \
|
||||
'limit[add limit route rule]' \
|
||||
&& ret=0
|
||||
;;
|
||||
(delete)
|
||||
rules="$(_ufw_delete_rules)"
|
||||
if [[ -n "$rules" ]] ; then
|
||||
@@ -109,6 +122,37 @@ case "$state" in
|
||||
'incoming' 'outgoing' \
|
||||
&& ret=0
|
||||
;;
|
||||
(route)
|
||||
case "$line[2]" in
|
||||
(delete|prepend)
|
||||
_values 'route-action' \
|
||||
'allow[route allow rule]' \
|
||||
'deny[route deny rule]' \
|
||||
'reject[route reject rule]' \
|
||||
'limit[route limit rule]' \
|
||||
&& ret=0
|
||||
;;
|
||||
(insert)
|
||||
_message 'route rule number'
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
(subsubsubcmds)
|
||||
case "$line[1]" in
|
||||
(route)
|
||||
case "$line[2]" in
|
||||
(insert)
|
||||
_values 'route-action' \
|
||||
'allow[route allow rule]' \
|
||||
'deny[route deny rule]' \
|
||||
'reject[route reject rule]' \
|
||||
'limit[route limit rule]' \
|
||||
&& ret=0
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
esac
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ typeset -g VI_MODE_CURSOR_OPPEND=${VI_MODE_CURSOR_OPPEND:=0}
|
||||
typeset -g VI_KEYMAP=${VI_KEYMAP:=main}
|
||||
|
||||
function _vi-mode-set-cursor-shape-for-keymap() {
|
||||
[[ "$VI_MODE_SET_CURSOR" = true ]] || return
|
||||
[[ "$VI_MODE_SET_CURSOR" = true ]] || return 0
|
||||
|
||||
# https://vt100.net/docs/vt510-rm/DECSCUSR
|
||||
local _shape=0
|
||||
|
||||
@@ -53,7 +53,7 @@ Available search contexts are:
|
||||
| `gopkg` | `https://pkg.go.dev/search?m=package&q=` |
|
||||
| `chatgpt` | `https://chatgpt.com/?q=` |
|
||||
| `claudeai` | `https://claude.ai/new?q=` |
|
||||
| `grok` | `https://grok.com/?q=` |
|
||||
| `grokcom` | `https://grok.com/?q=` |
|
||||
| `reddit` | `https://www.reddit.com/search/?q=` |
|
||||
| `ppai` | `https://www.perplexity.ai/search/new?q=` |
|
||||
| `rscrate` | `https://crates.io/search?q=` |
|
||||
|
||||
@@ -93,7 +93,7 @@ alias npmpkg='web_search npmpkg'
|
||||
alias packagist='web_search packagist'
|
||||
alias gopkg='web_search gopkg'
|
||||
alias chatgpt='web_search chatgpt'
|
||||
alias grok='web_search grok'
|
||||
alias grokcom='web_search grok'
|
||||
alias claudeai='web_search claudeai'
|
||||
alias reddit='web_search reddit'
|
||||
alias ppai='web_search ppai'
|
||||
|
||||
+82
-17
@@ -2,20 +2,26 @@
|
||||
|
||||
[](https://opensource.org/licenses/MIT)
|
||||

|
||||

|
||||

|
||||
[](https://github.com/agkozak/zsh-z/stargazers)
|
||||
|
||||

|
||||
|
||||
Zsh-z is a command-line tool that allows you to jump quickly to directories that you have visited frequently or recently -- but most often a combination of the two (a concept known as ["frecency"](https://en.wikipedia.org/wiki/Frecency)). It works by keeping track of when you go to directories and how much time you spend in them. Based on this data, it predicts where you want to go when you type a partial string. For example, `z src` might take you to `~/src/zsh`. `z zsh` might also get you there, and `z c/z` might prove to be even more specific -- it all depends on your habits and how long you have been using Zsh-z to build up a database. After using Zsh-z for a little while, you will get to where you want to be by typing considerably less than you would need to if you were using `cd`.
|
||||
|
||||
Zsh-z is a native Zsh port of [`rupa/z`](https://github.com/rupa/z), a tool written for `bash` and Zsh that uses embedded `awk` scripts to do the heavy lifting. `rupa/z` was my most used command-line tool for a couple of years. I decided to translate it, `awk` parts and all, into pure Zsh script, to see if by eliminating calls to external tools (`awk`, `sort`, `date`, `sed`, `mv`, `rm`, and `chown`) and reducing forking through subshells I could make it faster. The performance increase is impressive, particularly on systems where forking is slow, such as Cygwin, MSYS2, and WSL. I have found that in those environments, switching directories using Zsh-z can be over 100% faster than it is using `rupa/z`.
|
||||
Zsh-z is a native Zsh port of [`rupa/z`](https://github.com/rupa/z), a tool written for `bash` and Zsh that uses embedded `awk` scripts to do the heavy lifting. `rupa/z` was my most used command-line tool for a couple of years. I decided to translate it, `awk` parts and all, into pure Zsh script, to see if by eliminating calls to external tools (`awk`, `sort`, `date`, `sed`, `mv`, `rm`, and `chown`) and reducing forking through subshells I could make it faster. The performance increase is impressive, particularly on systems where forking is slow, such as Cygwin, MSYS2, and WSL.
|
||||
|
||||
There is also a significant stability improvement. Race conditions have always been a problem with `rupa/z`, and users of that utility occasionally lose their `~/.z` databases. By having Zsh-z only use Zsh (`rupa/z` uses a hybrid shell code standard that works on `bash` as well), I have been able to implement a `zsh/system`-based file-locking mechanism similar to [the one @mafredri once proposed for `rupa/z`](https://github.com/rupa/z/pull/199). It is now nearly impossible to crash the database.
|
||||
There is also a significant stability improvement. Race conditions have always been a problem with `rupa/z`, and users of that utility occasionally lose their `~/.z` databases. By having Zsh-z only use Zsh (`rupa/z` uses a hybrid shell code standard that works on `bash` as well), I have been able to implement a `zsh/system`-based file-locking mechanism. It is now nearly impossible to crash the database.
|
||||
|
||||
There are other, smaller improvements which I document below in [Improvements and Fixes](#improvements-and-fixes). For instance, tab completions are now sorted by frecency by default rather than alphabetically (the latter behavior can be restored if you like it -- [see below](#settings)).
|
||||
There are other, smaller improvements which I document below in [Other Improvements to the Original Functionality of `rupa/z`](#other-improvements-to-the-original-functionality-of-rupaz). For instance, tab completions are now sorted by frecency by default rather than alphabetically (the latter behavior can be restored if you like it -- [see below](#settings)).
|
||||
|
||||
Zsh-z is a drop-in replacement for `rupa/z` and will, by default, use the same database (`~/.z`, or whatever database file you specify), so you can go on using `rupa/z` when you launch `bash`.
|
||||
|
||||
> ### Zsh-z v2.0
|
||||
>
|
||||
> **v2.0 is the most significant release in the project's history.** It is faster than ever at adding, searching, and listing on modern Zsh; it never makes your prompt wait on database writes -- on any platform; it hardens those writes against corruption and against prying eyes; and it makes tab completion "just work" even under `setopt COMPLETE_ALIASES`. See [**v2.0**](#v20) in the News below for the full rundown, and [Performance](#performance) for the benchmarks.
|
||||
|
||||
## Table of Contents
|
||||
- [News](#news)
|
||||
- [Installation](#installation)
|
||||
@@ -23,16 +29,45 @@ Zsh-z is a drop-in replacement for `rupa/z` and will, by default, use the same d
|
||||
- [Settings](#settings)
|
||||
- [Case Sensitivity](#case-sensitivity)
|
||||
- [`ZSHZ_UNCOMMON`](#zshz_uncommon)
|
||||
- [`ZSHZ_OWNER`](#zshz_owner)
|
||||
- [Making `--add` work for you](#making---add-work-for-you)
|
||||
- [Other Improvements to the Original Functionality of `rupa/z`](#other-improvements-to-the-original-functionality-of-rupa-z)
|
||||
- [Performance](#performance)
|
||||
- [Other Improvements to the Original Functionality of `rupa/z`](#other-improvements-to-the-original-functionality-of-rupaz)
|
||||
- [Migrating from Other Tools](#migrating-from-other-tools)
|
||||
- [`COMPLETE_ALIASES`](#complete_aliases)
|
||||
|
||||
## News
|
||||
|
||||
<details>
|
||||
<summary>Here are the latest features and updates.</summary>
|
||||
### v2.0 (August 14, 2026)
|
||||
|
||||
Version **2.0** is a major step forward, and these are the changes most worth knowing about:
|
||||
|
||||
- **Zsh-z is even faster than before.** Version 2.0 builds on Zsh-z's past successes in optimizing adding to the datafile -- something it does at virtually every prompt -- by streamlining searching and listing, as well. See [Performance](#performance) for the numbers.
|
||||
- **Database writes never block your prompt** -- on any platform. The per-prompt `--add` has long run in the background on most systems, but Cygwin and MSYS2 did the write in the foreground, because backgrounding there cost a wrapper subshell plus a job. `--add` now runs as a single disowned job (`&!`) everywhere: one fork, no wrapper subshell, no job-control noise. On Cygwin and MSYS2 that turns a foreground write whose cost grows with your datafile (~30 ms at 300 entries, ~300 ms at 1,000) into a flat ~10-12 ms fork. Elsewhere it halves the forks per prompt.
|
||||
- **Safer, crash-resistant concurrent writes.** Writes are now guarded by a dedicated, stable lockfile using `zsh/system` file locking, with a bounded wait for lock acquisition (the new [`ZSHZ_LOCK_TIMEOUT`](#settings), default `1` second). Write errors are handled gracefully, and locks are always released even if a write is interrupted. On Cygwin and MSYS2 a write is also retried briefly if Windows refuses it: a virus scanner or the search indexer that opens the database in the instant between its being written and its being moved into place makes the move fail, which used to lose that one directory silently. Zsh-z now retries the move briefly.
|
||||
- **Your database file now has `600` permissions** -- readable and writable only by you -- so that other users on a shared system cannot read your directory history ([#92](https://github.com/agkozak/zsh-z/issues/92)). On Zsh 5+ this uses the in-process `zf_chmod` builtin; on Zsh 4.3.11 it uses a `umask`-in-a-subshell technique that avoids the fork-and-exec of an external `chmod`.
|
||||
- **`COMPLETE_ALIASES` works automatically in normal setups.** Tab completion no longer breaks when you have `setopt COMPLETE_ALIASES` enabled. Zsh-z registers the alias automatically on the first Tab press, so the manual `compdef` line that earlier versions required is no longer necessary. [See below](#complete_aliases).
|
||||
- **Fixed a `can't clobber parameter tmpfd` error on some Zsh builds.** On certain Zsh builds, every database write could fail with `can't clobber parameter tmpfd containing file descriptor 0`, leaving an error at each new prompt. The file descriptor used for the temporary database file is now held in an unset scalar rather than one seeded with `0`, so the write never trips Zsh's file-descriptor-clobber guard ([#81](https://github.com/agkozak/zsh-z/issues/81)).
|
||||
- **A misconfigured database file no longer closes your shell -- or nags you at every prompt.** When `ZSHZ_DATA` points at a directory, or names a file without a directory, Zsh-z now reports the problem and returns instead of calling `exit`. The per-prompt `--add` stays quiet about it, so you are told once, when you actually run `z`, rather than at every prompt ([#103](https://github.com/agkozak/zsh-z/issues/103); props @ahjota).
|
||||
- **`z -x` can now remove the entry for a directory that no longer exists -- and can no longer crash Zsh 4.3.11.** The removal target used to have to exist on disk, so a database entry whose directory had been deleted -- exactly the entry you most want gone -- could not be removed. `z -x /deleted/dir` (and `z -xR`) now canonicalizes the argument without requiring it to exist, resolving symlinks in as much of the path as is still present. The same change fixes a crash on Zsh 4.3.11, where an upstream bug makes `${x:A}` segfault when the top-level component of the path is missing: `z -x /gone/sub` -- or a `ZSHZ_DATA` pointing into a missing top-level directory -- could kill the shell there.
|
||||
- **More robust startup and operation.** A version check on an unsupported Zsh no longer risks exiting your interactive shell.
|
||||
|
||||
#### Smaller changes you may notice
|
||||
|
||||
None of these should need any action from you, but they are the things a v1 user is most likely to spot:
|
||||
|
||||
- **`-r` and `-t` can no longer be combined.** They name mutually exclusive sort orders -- rank versus recency -- so `z -rt foo` was always contradictory, and `-t` silently took precedence. It is now rejected with an error message instead of silently picking one.
|
||||
- **A lockfile appears next to your database.** Writes are serialized through `~/.z.lock` (or `$ZSHZ_DATA` plus `.lock`). It is created `600`, it stays empty, and it is deliberately never deleted -- removing a lockfile that another shell has already opened would reintroduce the very race it exists to prevent. Where `zsh/system` is missing and `zsystem flock` is therefore unavailable -- MobaXterm's cut-down Cygwin is the case in practice -- you will see a short-lived `~/.z.lock.d` **directory** instead, which is created and removed for each write. See [`ZSHZ_LOCK_TIMEOUT`](#settings) for how that fallback behaves.
|
||||
- **An existing database gets tightened to `600` on the next write.** If your `~/.z` predates this release and is `644`, the first write will re-mode it. This is the [#92](https://github.com/agkozak/zsh-z/issues/92) fix applied to databases you already have, not just newly created ones. Setting the mode is treated as part of the write rather than a courtesy afterwards: if it cannot be done, the write is abandoned and reports failure instead of publishing a database that anyone on the machine could read.
|
||||
|
||||
The dated entries below remain the historical record of changes leading up to v2.0.
|
||||
|
||||
<details>
|
||||
<summary>Here are the older features and updates.</summary>
|
||||
|
||||
- May 6, 2026
|
||||
+ Zsh-z will now handle paths with dollar signs (`$`) in them.
|
||||
+ Workaround for Zsh emulating `sh`/`bash`/`ksh`.
|
||||
- May 1, 2026
|
||||
+ Various tab completion bugs resolved.
|
||||
- April 27, 2026
|
||||
@@ -90,7 +125,7 @@ Zsh-z is a drop-in replacement for `rupa/z` and will, by default, use the same d
|
||||
- February 15, 2021
|
||||
+ Ranks are displayed the way `rupa/z` now displays them, i.e. as large integers. This should help Zsh-z to integrate with other tools.
|
||||
- January 31, 2021
|
||||
+ Zsh-z is now efficient enough that, on MSYS2 and Cygwin, it is faster to run it in the foreground than it is to fork a subshell for it.
|
||||
+ Zsh-z is now efficient enough that, on MSYS2 and Cygwin, it is faster to run it in the foreground than it is to fork a subshell for it. (Behavior superseded in v2.0.)
|
||||
+ `_zshz_precmd` simply returns if `PWD` is `HOME` or in `ZSHZ_EXCLUDE_DIRS`, rather than waiting for `zshz` to do that.
|
||||
- January 17, 2021
|
||||
+ Made sure that the `PUSHD_IGNORE_DUPS` option is respected.
|
||||
@@ -121,11 +156,11 @@ This plugin can be installed simply by putting the various files in a directory
|
||||
|
||||
source /path/to/zsh-z.plugin.zsh
|
||||
|
||||
For tab completion to work, `_zshz` *must* be in the same directory as `zsh-z.plugin.zsh`, and you will want to have loaded `compinit`. The frameworks handle this themselves. If you are not using a framework, put
|
||||
Tab completion requires `compinit`. `_zshz` *must* also be in the same directory as `zsh-z.plugin.zsh`. The frameworks handle both of these requirements, but if you are not using a framework, put
|
||||
|
||||
autoload -U compinit; compinit
|
||||
|
||||
in your `.zshrc` somewhere below where you source `zsh-z.plugin.zsh`.
|
||||
in your `.zshrc` somewhere below where you source `zsh-z.plugin.zsh` -- the plugin adds its directory to `fpath` at source time, and `compinit` needs to see it there in order to find `_zshz`.
|
||||
|
||||
If you add
|
||||
|
||||
@@ -235,7 +270,7 @@ Add the line
|
||||
|
||||
to your `.zshrc`.
|
||||
|
||||
Zsh-z supports `zinit`'s `unload` feature; just run `zinit unload agkozak/zsh-z` to restore the shell to its state before Zsh-z was loaded.
|
||||
Zsh-z supports `zinit`'s `unload` feature; just run `zinit unload agkozak/zsh-z` to restore the shell to its state before Zsh-z was loaded. It gives back only what it took: the `fpath` entry is removed only if Zsh-z was the one that added it, so a directory your plugin manager put there is left alone, and the Tab-completion mapping is dropped only while it still points at Zsh-z's own completer.
|
||||
|
||||
### For [Znap](https://github.com/marlonrichert/zsh-snap) users
|
||||
|
||||
@@ -274,6 +309,7 @@ to install Zsh-z.
|
||||
|
||||
Zsh-z has environment variables (they all begin with `ZSHZ_`) that change its behavior if you set them. You can also keep your old ones if you have been using `rupa/z` (whose environment variables begin with `_Z_`).
|
||||
|
||||
* `ZSHZ_CASE` can be `ignore`, for case-insensitive matching, or `smart`, for Vim-like `smartcase` matching; [see below](#case-sensitivity) (default: empty, i.e., a case-sensitive match is tried first, then a case-insensitive one)
|
||||
* `ZSHZ_CMD` changes the command name (default: `z`)
|
||||
* `ZSHZ_CD` specifies the default directory-changing command (default: `builtin cd`)
|
||||
* `ZSHZ_COMPLETION` can be `'frecent'` (default) or `'legacy'`, depending on whether you want your completion results sorted according to frecency or simply sorted alphabetically
|
||||
@@ -281,9 +317,10 @@ Zsh-z has environment variables (they all begin with `ZSHZ_`) that change its be
|
||||
* `ZSHZ_ECHO` displays the new path name when changing directories (default: `0`)
|
||||
* `ZSHZ_EXCLUDE_DIRS` is an array of directories to keep out of the database (default: empty)
|
||||
* `ZSHZ_KEEP_DIRS` is an array of directories that should not be removed from the database, even if they are not currently available (useful when a drive is not always mounted) (default: empty)
|
||||
* `ZSHZ_LOCK_TIMEOUT` is the number of seconds to wait for the database lock before giving up on a write (default: `1`). Where `zsh/system` is unavailable and `zsystem flock` cannot be used -- MobaXterm's cut-down Cygwin, for instance -- Zsh-z serializes writes with an atomic `mkdir` on `~/.z.lock.d` instead, and this setting bounds the wait the same way. That fallback has no equivalent of the kernel releasing a lock when its holder dies, so a lock directory older than 30 seconds is treated as abandoned and cleared; a write takes milliseconds, so nothing legitimate is ever that old. A write that times out is dropped silently -- the automatic `precmd` add is best-effort -- so if the database seems to stop updating, the lock is probably contended: run `z --add .` by hand and check `$?`. A `2` is a lock-acquisition timeout, which confirms contention -- look for a stale process holding `~/.z.lock` (or a leftover `~/.z.lock.d` directory on the fallback path), or raise this setting. A `1` is something else entirely, usually a permissions or ownership problem, such as a root-owned `~/.z` or `~/.z.lock` left behind by an earlier `sudo -s` session.
|
||||
* `ZSHZ_MAX_SCORE` is the maximum combined score the database entries can have before they begin to age and potentially drop out of the database (default: 9000)
|
||||
* `ZSHZ_NO_RESOLVE_SYMLINKS` prevents symlink resolution (default: `0`)
|
||||
* `ZSHZ_OWNER` allows usage when in `sudo -s` mode (default: empty)
|
||||
* `ZSHZ_OWNER` is the username the database belongs to; set it to your own login name to keep `z` working in a root shell, such as under `sudo -E -s`; [see below](#zshz_owner) (default: empty)
|
||||
* `ZSHZ_TILDE` displays the name of the `HOME` directory as a `~` (default: `0`)
|
||||
* `ZSHZ_TRAILING_SLASH` makes it so that a search pattern ending in `/` can match the final element in a path; e.g., `z foo/` can match `/path/to/foo` (default: `0`)
|
||||
* `ZSHZ_UNCOMMON` changes the logic used to calculate the directory jumped to; [see below](#zshz_uncommon) (default: `0`)
|
||||
@@ -319,6 +356,12 @@ then there is no common prefix. In this case, `z code` will simply send you to t
|
||||
|
||||
You may enable an alternate, experimental behavior by setting `ZSHZ_UNCOMMON=1`. If you do that, Zsh-z will not jump to a common prefix, even if one exists. Instead, it chooses the highest-ranking match -- but it drops any subdirectories that do not include the search term. So if you type `z bat` and `/home/me/code/bat` is the best match, that is exactly where you will end up. If, however, you had typed `z code` and the best match was also `/home/me/code/bat`, you would have ended up in `/home/me/code` (because `code` was what you had searched for). This feature is still in development, and feedback is welcome.
|
||||
|
||||
## `ZSHZ_OWNER`
|
||||
|
||||
If you are using `root` privileges while keeping your personal home directory as `HOME` (as is the case with `sudo -E -s`), conflicts with file ownership arise. You can resolve them by using `ZSHZ_OWNER`. If you set this variable to your username, Zsh-z will be able to use your personal datafile, restoring its proper ownership with every write.
|
||||
|
||||
Setting `ZSHZ_OWNER` makes one thing stricter, because a privileged shell is then writing to a path that an unprivileged user controls: Zsh-z follows a symlink on the way to the datafile -- the file itself, or any parent directory -- only if `root` owns the link, and reports an error instead of writing otherwise. Symlinked system directories, such as `/home` → `/usr/home` on the BSDs or `/var` → `/private/var` on macOS, belong to root and still resolve. With `ZSHZ_OWNER` unset none of this applies: a symlinked `~/.z` is followed exactly as before, so pointing it at synced storage keeps working.
|
||||
|
||||
## Making `--add` Work for You
|
||||
|
||||
Zsh-z internally uses the `--add` option to add paths to its database. @zachriggle pointed out to me that users might want to use `--add` themselves, so I have altered it a little to make it more user-friendly.
|
||||
@@ -331,9 +374,33 @@ A good example might involve a directory tree that has Git repositories within i
|
||||
|
||||
(As a Zsh user, I tend to use `**` instead of `find`, but it is good to see how deep your directory trees go before doing that.)
|
||||
|
||||
## Performance
|
||||
|
||||
One of the goals of the rewrite that culminated in v2.0 was to make Zsh-z simultaneously more stable and faster. On modern Zsh, Zsh-z outpaces `rupa/z`'s `z.sh` at adding, searching, and listing. Representative figures (N = 200 database entries, medians of seven interleaved runs on a Core i7-12700 desktop under WSL2):
|
||||
|
||||
**Modern Zsh (5.9) -- Zsh-z vs. `rupa/z`:**
|
||||
|
||||
| Operation | `rupa/z` (`z.sh`) | Zsh-z | Winner |
|
||||
| --------- | ----------------- | ----------- | ----------------- |
|
||||
| `add` | 6.25 ms/op | 1.99 ms/op | **Zsh-z** ~3.14x |
|
||||
| `search` | 6.51 ms/op | 3.62 ms/op | **Zsh-z** ~1.80x |
|
||||
| `list` | 8.93 ms/op | 4.36 ms/op | **Zsh-z** ~2.05x |
|
||||
|
||||
**Zsh 4.3.11 (the oldest supported release) -- Zsh-z vs. `rupa/z`:**
|
||||
|
||||
| Operation | `rupa/z` (`z.sh`) | Zsh-z | Winner |
|
||||
| --------- | ----------------- | ----------- | ----------------- |
|
||||
| `add` | 5.00 ms/op | 2.92 ms/op | **Zsh-z** ~1.71x |
|
||||
| `search` | 4.93 ms/op | 4.47 ms/op | **Zsh-z** ~1.10x |
|
||||
| `list` | 7.92 ms/op | 6.08 ms/op | **Zsh-z** ~1.30x |
|
||||
|
||||
Removal is absent from these tables: in `rupa/z`, the per-prompt hook re-adds the current directory right after `z -x` removes it, so the two implementations' `-x` are not doing comparable work.
|
||||
|
||||
Relative to the previous generation of Zsh-z, the v2.0 read path is dramatically faster -- on modern Zsh, listing the whole database is about 2.4x faster and searching about 1.6x faster.
|
||||
|
||||
## Other Improvements to the Original Functionality of `rupa/z`
|
||||
|
||||
* `z -x` works, with the help of `chpwd_functions`.
|
||||
* `z -x` works: a directory you remove stays removed.
|
||||
* Zsh-z is compatible with Solaris.
|
||||
* Zsh-z uses the "new" `zshcompsys` completion system instead of the old `compctl` one.
|
||||
* No error message is displayed when the database file has not yet been created.
|
||||
@@ -355,12 +422,10 @@ If you are coming to Zsh-z (or even to the original `rupa/z`, for that matter) f
|
||||
|
||||
## `COMPLETE_ALIASES`
|
||||
|
||||
`z`, or any alternative you set up using `$ZSHZ_CMD` or `$_Z_CMD`, is an alias. `setopt COMPLETE_ALIASES` divorces the tab completion for aliases from the underlying commands they invoke, so if you enable `COMPLETE_ALIASES`, tab completion for Zsh-z will be broken. You can get it working again, however, by adding under
|
||||
`z`, or any alternative you set up using `$ZSHZ_CMD` or `$_Z_CMD`, is an alias. `setopt COMPLETE_ALIASES` divorces the tab completion for aliases from the underlying commands they invoke, which historically broke Zsh-z's tab completion. Zsh-z now handles this automatically: the first Tab press registers the alias name with `_zshz` so completion works under `COMPLETE_ALIASES` without any extra setup.
|
||||
|
||||
setopt COMPLETE_ALIASES
|
||||
|
||||
the line
|
||||
That registration happens inside Zsh-z's own Tab widget. If a plugin loaded after Zsh-z replaces the Tab binding without invoking the previous widget, Zsh-z's widget never runs and the registration does not happen. Under `COMPLETE_ALIASES`, you would then have no completion for `z`. Once `compinit` has run, adding
|
||||
|
||||
compdef _zshz ${ZSHZ_CMD:-${_Z_CMD:-z}}
|
||||
|
||||
That will re-bind `z` or the command of your choice to the underlying Zsh-z function.
|
||||
below `setopt COMPLETE_ALIASES` in your `.zshrc` fixes that, and it is harmless if the automatic registration has already run.
|
||||
|
||||
+2
-6
@@ -1,6 +1,6 @@
|
||||
#compdef zshz ${ZSHZ_CMD:-${_Z_CMD:-z}}
|
||||
#
|
||||
# Zsh-z - jump around with Zsh - A native Zsh version of z without awk, sort,
|
||||
# Zsh-z - jump around with Zsh - A native Zsh version of rupa/z without awk, sort,
|
||||
# date, or sed
|
||||
#
|
||||
# https://github.com/agkozak/zsh-z
|
||||
@@ -24,11 +24,7 @@
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
#
|
||||
# z (https://github.com/rupa/z) is copyright (c) 2009 rupa deadwyler and
|
||||
# licensed under the WTFPL license, Version 2.a
|
||||
#
|
||||
# shellcheck shell=ksh
|
||||
################################################################################
|
||||
|
||||
############################################################
|
||||
# Zsh-z COMPLETIONS
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user