mirror of
https://github.com/ohmyzsh/ohmyzsh.git
synced 2026-08-07 06:08:39 +02:00
Merge 1f840eca9d into 076d692dac
This commit is contained in:
commit
cc710e1732
3 changed files with 398 additions and 9 deletions
|
|
@ -90,13 +90,59 @@ ZSH_DOTENV_ALLOWED_LIST=/path/to/dotenv/allowed/list
|
|||
ZSH_DOTENV_DISALLOWED_LIST=/path/to/dotenv/disallowed/list
|
||||
```
|
||||
|
||||
The file is just a list of directories, separated by a newline character. If you want
|
||||
to change your decision, just edit the file and remove the line for the directory you want to
|
||||
change.
|
||||
The plugin creates these files automatically. You can inspect or update them with:
|
||||
|
||||
- `dotenv-list [allowed|disallowed]`: show the list file location and entries. Without an
|
||||
argument, it shows both lists.
|
||||
- `dotenv-edit [allowed|disallowed]`: open a list in `$VISUAL`, `$EDITOR` or `vi`.
|
||||
Without an argument, it opens the allowed list.
|
||||
- `dotenv-allow [path]`: add a directory to the allowed list. Without an argument, it
|
||||
adds the current directory.
|
||||
- `dotenv-disallow [path]`: add a directory to the disallowed list. Without an argument,
|
||||
it adds the current directory.
|
||||
- `dotenv-allow --pattern 'pattern'` and `dotenv-disallow --pattern 'pattern'`: add a
|
||||
zsh filename pattern instead of a literal path.
|
||||
|
||||
Paths added by `dotenv-allow`, `dotenv-disallow`, [a]lways or n[e]ver are escaped so glob
|
||||
metacharacters in real directory names are treated literally. Use `--pattern` only when you
|
||||
intentionally want wildcard matching.
|
||||
|
||||
If you edit the files directly, use one absolute directory path or pattern per line.
|
||||
Lines starting with `#` are treated as comments. Blank lines are ignored, and leading and
|
||||
trailing whitespace around an entry is stripped.
|
||||
|
||||
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
|
||||
|
||||
Exact absolute directory paths still match literally. Other entries in the allowed and
|
||||
disallowed list files are expanded with zsh filename globbing and matched against the
|
||||
current directory's absolute path. This keeps wildcard matching limited to actual directory
|
||||
paths instead of arbitrary string prefixes.
|
||||
|
||||
For example, if you use [git worktrees](https://git-scm.com/docs/git-worktree) and all your
|
||||
worktrees live under a common prefix, add a single pattern instead of allowing each one
|
||||
individually:
|
||||
|
||||
```sh
|
||||
dotenv-allow --pattern '/Users/me/Dev/my-project-wt-*'
|
||||
```
|
||||
|
||||
With filename globbing, `*` matches one path component: `/Users/me/Dev/my-project-wt-*`
|
||||
matches `/Users/me/Dev/my-project-wt-auth`, but not
|
||||
`/Users/me/Dev/my-project-wt-auth/api`. To match nested directories, include the nested
|
||||
path component in the pattern, for example `/Users/me/Dev/my-project-wt-*/*`.
|
||||
|
||||
Patterns must be absolute paths or start with `~`. The basic zsh filename pattern operators
|
||||
are supported, such as `*`, `?`, character classes like `[abc]`, and alternation like
|
||||
`(foo|bar)`. Operators that require `EXTENDED_GLOB` (such as `#`, `^` and pattern
|
||||
exclusion `~`) are not enabled by the plugin.
|
||||
|
||||
If a manually edited literal path contains pattern metacharacters (`*`, `?`, `[`, `(`,
|
||||
etc.), escape them with a backslash to match the path exactly. Malformed patterns are
|
||||
treated as non-matching.
|
||||
|
||||
## Named Pipe (FIFO) Support
|
||||
|
||||
The plugin supports `.env` files provided as UNIX named pipes (FIFOs) in addition to regular files.
|
||||
|
|
|
|||
|
|
@ -272,6 +272,199 @@ _dotenv_check_syntax() {
|
|||
}
|
||||
}
|
||||
|
||||
_dotenv_list_match() {
|
||||
emulate -L zsh
|
||||
local dirpath="${1:A}" list_file=$2 line match
|
||||
local -a matches
|
||||
|
||||
[[ -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
|
||||
[[ "$line" == /* || "$line" == ~* ]] || continue
|
||||
[[ "$line" == /* && "${line:A}" == "$dirpath" ]] && return 0
|
||||
|
||||
matches=()
|
||||
{
|
||||
matches=(${~line}(N-/))
|
||||
} always {
|
||||
if (( TRY_BLOCK_ERROR )); then
|
||||
TRY_BLOCK_ERROR=0
|
||||
matches=()
|
||||
fi
|
||||
}
|
||||
|
||||
for match in "${matches[@]}"; do
|
||||
[[ "${match:A}" == "$dirpath" ]] && return 0
|
||||
done
|
||||
done < "$list_file" 2>/dev/null
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
_dotenv_ensure_list_file() {
|
||||
emulate -L zsh
|
||||
local list_file=$1
|
||||
|
||||
[[ -n "$list_file" ]] || return 1
|
||||
command mkdir -p -- "${list_file:h}" || return 1
|
||||
[[ -e "$list_file" ]] || command touch -- "$list_file" || return 1
|
||||
}
|
||||
|
||||
_dotenv_list_file_for() {
|
||||
emulate -L zsh
|
||||
|
||||
case "$1" in
|
||||
allowed|allow)
|
||||
REPLY="$ZSH_DOTENV_ALLOWED_LIST"
|
||||
;;
|
||||
disallowed|disallow|denied|deny)
|
||||
REPLY="$ZSH_DOTENV_DISALLOWED_LIST"
|
||||
;;
|
||||
*)
|
||||
echo "dotenv: expected 'allowed' or 'disallowed'" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_dotenv_append_list_entry() {
|
||||
emulate -L zsh
|
||||
local list_file=$1 entry=$2
|
||||
|
||||
_dotenv_ensure_list_file "$list_file" || return 1
|
||||
|
||||
if command grep -Fx -q -- "$entry" "$list_file" 2>/dev/null; then
|
||||
REPLY=present
|
||||
return
|
||||
fi
|
||||
|
||||
print -r -- "$entry" >> "$list_file" || return 1
|
||||
REPLY=added
|
||||
}
|
||||
|
||||
_dotenv_add_list_entry_usage() {
|
||||
print -r -- "Usage: $1 [--pattern] [path-or-pattern]"
|
||||
}
|
||||
|
||||
_dotenv_add_list_entry_command() {
|
||||
emulate -L zsh
|
||||
local list_file=$1 command_name=$2 entry list_entry resolved_path
|
||||
local as_pattern=false
|
||||
shift 2
|
||||
|
||||
if [[ "$1" == -h || "$1" == --help ]]; then
|
||||
_dotenv_add_list_entry_usage "$command_name"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ "$1" == --pattern ]]; then
|
||||
as_pattern=true
|
||||
shift
|
||||
fi
|
||||
|
||||
if (( $# > 1 )) || [[ "$as_pattern" == true && $# -ne 1 ]]; then
|
||||
_dotenv_add_list_entry_usage "$command_name" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
entry="${1:-$PWD}"
|
||||
if [[ "$as_pattern" == true ]]; then
|
||||
if [[ "$entry" != /* && "$entry" != ~* ]]; then
|
||||
echo "dotenv: patterns must be absolute paths or start with '~'" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
list_entry="$entry"
|
||||
else
|
||||
resolved_path="${entry:A}"
|
||||
list_entry="${(b)resolved_path}"
|
||||
fi
|
||||
|
||||
_dotenv_append_list_entry "$list_file" "$list_entry" || return 1
|
||||
|
||||
case "$REPLY" in
|
||||
added) echo "dotenv: added '$list_entry' to $list_file" ;;
|
||||
present) echo "dotenv: '$list_entry' is already in $list_file" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
dotenv-allow() {
|
||||
_dotenv_add_list_entry_command "$ZSH_DOTENV_ALLOWED_LIST" dotenv-allow "$@"
|
||||
}
|
||||
|
||||
dotenv-disallow() {
|
||||
_dotenv_add_list_entry_command "$ZSH_DOTENV_DISALLOWED_LIST" dotenv-disallow "$@"
|
||||
}
|
||||
|
||||
_dotenv_print_list() {
|
||||
emulate -L zsh
|
||||
local list_name=$1 list_file line
|
||||
|
||||
_dotenv_list_file_for "$list_name" || return 1
|
||||
list_file="$REPLY"
|
||||
_dotenv_ensure_list_file "$list_file" || return 1
|
||||
|
||||
echo "$list_name: $list_file"
|
||||
if [[ ! -s "$list_file" ]]; then
|
||||
echo " <empty>"
|
||||
return
|
||||
fi
|
||||
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
print -r -- " $line"
|
||||
done < "$list_file"
|
||||
}
|
||||
|
||||
dotenv-list() {
|
||||
emulate -L zsh
|
||||
|
||||
if [[ "$1" == -h || "$1" == --help ]]; then
|
||||
echo "Usage: dotenv-list [allowed|disallowed]"
|
||||
return
|
||||
fi
|
||||
|
||||
case $# in
|
||||
0)
|
||||
_dotenv_print_list allowed
|
||||
_dotenv_print_list disallowed
|
||||
;;
|
||||
1)
|
||||
_dotenv_print_list "$1"
|
||||
;;
|
||||
*)
|
||||
echo "Usage: dotenv-list [allowed|disallowed]" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
dotenv-edit() {
|
||||
emulate -L zsh
|
||||
local list_name="${1:-allowed}" list_file editor
|
||||
|
||||
if [[ "$1" == -h || "$1" == --help ]]; then
|
||||
echo "Usage: dotenv-edit [allowed|disallowed]"
|
||||
return
|
||||
fi
|
||||
|
||||
if (( $# > 1 )); then
|
||||
echo "Usage: dotenv-edit [allowed|disallowed]" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
_dotenv_list_file_for "$list_name" || return 1
|
||||
list_file="$REPLY"
|
||||
_dotenv_ensure_list_file "$list_file" || return 1
|
||||
|
||||
editor="${VISUAL:-${EDITOR:-vi}}"
|
||||
${=editor} "$list_file"
|
||||
}
|
||||
|
||||
source_env() {
|
||||
if [[ ! -f "$ZSH_DOTENV_FILE" ]] && [[ ! -p "$ZSH_DOTENV_FILE" ]]; then
|
||||
return
|
||||
|
|
@ -281,16 +474,16 @@ source_env() {
|
|||
local confirmation dirpath="${PWD:A}"
|
||||
|
||||
# make sure there is an (dis-)allowed file
|
||||
touch "$ZSH_DOTENV_ALLOWED_LIST"
|
||||
touch "$ZSH_DOTENV_DISALLOWED_LIST"
|
||||
_dotenv_ensure_list_file "$ZSH_DOTENV_ALLOWED_LIST" || return 1
|
||||
_dotenv_ensure_list_file "$ZSH_DOTENV_DISALLOWED_LIST" || return 1
|
||||
|
||||
# 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 +499,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
|
||||
|
|
|
|||
150
plugins/dotenv/tests/allow-list.zunit
Normal file
150
plugins/dotenv/tests/allow-list.zunit
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
#!/usr/bin/env zunit
|
||||
|
||||
@setup {
|
||||
typeset -g tmpdir="$(mktemp -d "${TMPDIR:-/tmp}/dotenv-list.XXXXXX")"
|
||||
typeset -g allowed_list="$tmpdir/allowed.list"
|
||||
typeset -g disallowed_list="$tmpdir/disallowed.list"
|
||||
ZSH_DOTENV_ALLOWED_LIST="$allowed_list"
|
||||
ZSH_DOTENV_DISALLOWED_LIST="$disallowed_list"
|
||||
}
|
||||
|
||||
@teardown {
|
||||
[[ -n "$tmpdir" && -d "$tmpdir" ]] && command rm -rf "$tmpdir"
|
||||
unset DOTENV_PRECEDENCE 2>/dev/null
|
||||
}
|
||||
|
||||
@test 'list match supports escaped literal paths' {
|
||||
local dir="$tmpdir/project[one]"
|
||||
command mkdir -p "$dir"
|
||||
print -r -- "${(b)dir}" > "$allowed_list"
|
||||
|
||||
run _dotenv_list_match "$dir" "$allowed_list"
|
||||
assert $state equals 0
|
||||
}
|
||||
|
||||
@test 'list match keeps exact absolute entries literal' {
|
||||
local dir="$tmpdir/project[one]"
|
||||
command mkdir -p "$dir"
|
||||
print -r -- "$dir" > "$allowed_list"
|
||||
|
||||
run _dotenv_list_match "$dir" "$allowed_list"
|
||||
assert $state equals 0
|
||||
}
|
||||
|
||||
@test 'list match ignores comments blank lines and CRLF endings' {
|
||||
local dir="$tmpdir/project"
|
||||
command mkdir -p "$dir"
|
||||
{
|
||||
print -r -- ' # comment'
|
||||
print -r -- ' '
|
||||
printf '%s\r\n' " ${(b)dir} "
|
||||
} > "$allowed_list"
|
||||
|
||||
run _dotenv_list_match "$dir" "$allowed_list"
|
||||
assert $state equals 0
|
||||
}
|
||||
|
||||
@test 'filename glob matches direct directories only' {
|
||||
local project="$tmpdir/worktrees/app-one"
|
||||
local nested="$project/nested"
|
||||
command mkdir -p "$nested"
|
||||
print -r -- "$tmpdir/worktrees/*" > "$allowed_list"
|
||||
|
||||
run _dotenv_list_match "$project" "$allowed_list"
|
||||
assert $state equals 0
|
||||
|
||||
run _dotenv_list_match "$nested" "$allowed_list"
|
||||
assert $state equals 1
|
||||
}
|
||||
|
||||
@test 'filename glob can match nested directories explicitly' {
|
||||
local nested="$tmpdir/worktrees/app-one/nested"
|
||||
command mkdir -p "$nested"
|
||||
print -r -- "$tmpdir/worktrees/*/nested" > "$allowed_list"
|
||||
|
||||
run _dotenv_list_match "$nested" "$allowed_list"
|
||||
assert $state equals 0
|
||||
}
|
||||
|
||||
@test 'malformed patterns are ignored without breaking later entries' {
|
||||
local dir="$tmpdir/project"
|
||||
command mkdir -p "$dir"
|
||||
print -r -- "$tmpdir/[" > "$allowed_list"
|
||||
print -r -- "${(b)dir}" >> "$allowed_list"
|
||||
|
||||
run _dotenv_list_match "$dir" "$allowed_list"
|
||||
assert $state equals 0
|
||||
}
|
||||
|
||||
@test 'relative entries are ignored' {
|
||||
print -r -- '.' > "$allowed_list"
|
||||
|
||||
run _dotenv_list_match "$PWD" "$allowed_list"
|
||||
assert $state equals 1
|
||||
}
|
||||
|
||||
@test 'disallowed entries take precedence over allowed entries' {
|
||||
local project="$tmpdir/project"
|
||||
command mkdir -p "$project"
|
||||
print -r -- 'DOTENV_PRECEDENCE=loaded' > "$project/.env"
|
||||
print -r -- "${(b)project}" > "$allowed_list"
|
||||
print -r -- "${(b)project}" > "$disallowed_list"
|
||||
|
||||
(
|
||||
ZSH_DOTENV_FILE=.env
|
||||
ZSH_DOTENV_PROMPT=true
|
||||
cd "$project"
|
||||
source_env
|
||||
[[ ! -v DOTENV_PRECEDENCE ]]
|
||||
)
|
||||
assert $? equals 0
|
||||
}
|
||||
|
||||
@test 'dotenv-allow writes escaped literal absolute paths' {
|
||||
local dir="$tmpdir/project[one]"
|
||||
local resolved_dir
|
||||
command mkdir -p "$dir"
|
||||
resolved_dir="${dir:A}"
|
||||
|
||||
dotenv-allow "$dir" >/dev/null
|
||||
|
||||
assert "$(<"$allowed_list")" equals "${(b)resolved_dir}"
|
||||
}
|
||||
|
||||
@test 'dotenv-disallow with pattern writes the raw pattern' {
|
||||
local pattern="$tmpdir/worktrees/*"
|
||||
|
||||
dotenv-disallow --pattern "$pattern" >/dev/null
|
||||
|
||||
assert "$(<"$disallowed_list")" equals "$pattern"
|
||||
}
|
||||
|
||||
@test 'dotenv-allow does not append duplicate entries' {
|
||||
local dir="$tmpdir/project"
|
||||
local -a lines
|
||||
command mkdir -p "$dir"
|
||||
|
||||
dotenv-allow "$dir" >/dev/null
|
||||
dotenv-allow "$dir" >/dev/null
|
||||
lines=("${(@f)"$(<"$allowed_list")"}")
|
||||
|
||||
assert ${#lines} equals 1
|
||||
}
|
||||
|
||||
@test 'dotenv-list shows list location and entries' {
|
||||
local output
|
||||
print -r -- "$tmpdir/project" > "$allowed_list"
|
||||
|
||||
output="$(dotenv-list allowed)"
|
||||
|
||||
[[ "$output" == *"$allowed_list"* && "$output" == *"$tmpdir/project"* ]]
|
||||
assert $? equals 0
|
||||
}
|
||||
|
||||
@test 'dotenv-edit opens the selected list with the configured editor' {
|
||||
VISUAL= EDITOR=true dotenv-edit disallowed
|
||||
assert $? equals 0
|
||||
|
||||
[[ -f "$disallowed_list" ]]
|
||||
assert $? equals 0
|
||||
}
|
||||
Loading…
Reference in a new issue