From 8d5b8c21578e25291735b2c99c0b93f3b1dbaf17 Mon Sep 17 00:00:00 2001 From: Thomas Witt Date: Thu, 12 Feb 2026 16:59:11 +0200 Subject: [PATCH] feat(dotenv): support glob patterns in allowed and disallowed lists The allowed and disallowed list files now support zsh glob patterns (e.g., /path/to/projects/*) in addition to exact paths. This is useful for git worktree setups and other workflows where multiple directories share a common prefix. Comment lines and blank lines are now also supported in list files. --- plugins/dotenv/README.md | 29 +++++++++++++++++++++++++ plugins/dotenv/dotenv.plugin.zsh | 36 ++++++++++++++++++++++++++++---- 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/plugins/dotenv/README.md b/plugins/dotenv/README.md index 8b3f9ecce..a9c9937a2 100644 --- a/plugins/dotenv/README.md +++ b/plugins/dotenv/README.md @@ -97,6 +97,35 @@ change. NOTE: if a directory is found in both the allowed and disallowed lists, the disallowed list takes preference, _i.e._ the .env file will never be sourced. +### Glob/Wildcard Patterns + +Entries in the allowed and disallowed list files are matched as zsh patterns against the +directory path, so wildcards work in addition to exact paths. This is useful when you want +to allow or disallow entire directory trees at once. + +For example, if you use [git worktrees](https://git-scm.com/docs/git-worktree) and all your +worktrees live under a common prefix, you can add a single pattern instead of allowing each +one individually: + +```sh +# In your dotenv-allowed.list file: +/Users/me/Dev/my-project-wt-* +``` + +Note that entries are matched against the whole path as a string (as in +`[[ $dir == pattern ]]`), not with filename globbing: `*` and `?` match any characters +**including `/`**, so `/Users/me/*` also matches nested directories like `/Users/me/a/b`. +The basic zsh pattern operators are supported: `*`, `?`, character classes like `[abc]`, +and alternation like `(foo|bar)`. Operators that require `EXTENDED_GLOB` (such as `#`, +`^` and `~`) are **not** enabled by the plugin. + +If a literal path contains pattern metacharacters (`*`, `?`, `[`, `(`, etc.), escape them +with a backslash to match the path exactly. Paths added by answering [a]lways or n[e]ver +at the prompt are escaped automatically. Malformed patterns are treated as non-matching. + +Lines starting with `#` are treated as comments. Blank lines are ignored, and leading and +trailing whitespace around an entry is stripped. + ## Named Pipe (FIFO) Support The plugin supports `.env` files provided as UNIX named pipes (FIFOs) in addition to regular files. diff --git a/plugins/dotenv/dotenv.plugin.zsh b/plugins/dotenv/dotenv.plugin.zsh index 72839a501..d4a73b517 100644 --- a/plugins/dotenv/dotenv.plugin.zsh +++ b/plugins/dotenv/dotenv.plugin.zsh @@ -272,6 +272,34 @@ _dotenv_check_syntax() { } } +_dotenv_list_match() { + emulate -L zsh + local dirpath=$1 list_file=$2 line + local -i matched + + [[ -r $list_file ]] || return 1 + + while IFS= read -r line || [[ -n $line ]]; do + # tolerate CRLF line endings and surrounding whitespace + line="${line%$'\r'}" + line="${line#"${line%%[![:space:]]*}"}" + line="${line%"${line##*[![:space:]]}"}" + [[ -z "$line" || "$line" == \#* ]] && continue + + # a malformed pattern raises a fatal zsh error; contain it and + # treat the entry as non-matching + matched=1 + { + [[ $dirpath == ${~line} ]] && matched=0 + } always { + (( TRY_BLOCK_ERROR )) && TRY_BLOCK_ERROR=0 + } + (( matched )) || return 0 + done < "$list_file" 2>/dev/null + + return 1 +} + source_env() { if [[ ! -f "$ZSH_DOTENV_FILE" ]] && [[ ! -p "$ZSH_DOTENV_FILE" ]]; then return @@ -285,12 +313,12 @@ source_env() { touch "$ZSH_DOTENV_DISALLOWED_LIST" # early return if disallowed - if command grep -Fx -q "$dirpath" "$ZSH_DOTENV_DISALLOWED_LIST" &>/dev/null; then + if _dotenv_list_match "$dirpath" "$ZSH_DOTENV_DISALLOWED_LIST"; then return fi # check if current directory's .env file is allowed or ask for confirmation - if ! command grep -Fx -q "$dirpath" "$ZSH_DOTENV_ALLOWED_LIST" &>/dev/null; then + if ! _dotenv_list_match "$dirpath" "$ZSH_DOTENV_ALLOWED_LIST"; then # get cursor column and print new line before prompt if not at line beginning local column echo -ne "\e[6n" > /dev/tty @@ -306,8 +334,8 @@ source_env() { # check input case "$confirmation" in [yY]) ;; - [aA]) echo "$dirpath" >> "$ZSH_DOTENV_ALLOWED_LIST" ;; - [eE]) echo "$dirpath" >> "$ZSH_DOTENV_DISALLOWED_LIST"; return ;; + [aA]) print -r -- "${(b)dirpath}" >> "$ZSH_DOTENV_ALLOWED_LIST" ;; + [eE]) print -r -- "${(b)dirpath}" >> "$ZSH_DOTENV_DISALLOWED_LIST"; return ;; *) return ;; # interpret anything else as a no esac fi