12 # Subcommand functions start with _ so that they don't
13 # appear as completion entries when looking for `omz`
14 (( $+functions[_omz::$command] )) || {
25 'changelog:Print the changelog'
26 'help:Usage information'
27 'plugin:Manage plugins'
28 'pr:Manage Oh My Zsh Pull Requests'
29 'reload:Reload the current zsh session'
31 'update:Update Oh My Zsh'
32 'version:Show the version'
35 if (( CURRENT == 2 )); then
36 _describe 'command' cmds
37 elif (( CURRENT == 3 )); then
39 changelog) local -a refs
40 refs=("${(@f)$(builtin cd -q "$ZSH"; command git for-each-ref --format="%(refname:short):%(subject)" refs/heads refs/tags)}")
41 _describe 'command' refs ;;
43 'disable:Disable plugin(s)'
44 'enable:Enable plugin(s)'
45 'info:Get plugin information'
49 _describe 'command' subcmds ;;
50 pr) subcmds=('clean:Delete all Pull Request branches' 'test:Test a Pull Request')
51 _describe 'command' subcmds ;;
52 theme) subcmds=('list:List themes' 'set:Set a theme in your .zshrc file' 'use:Load a theme')
53 _describe 'command' subcmds ;;
55 elif (( CURRENT == 4 )); then
56 case "${words[2]}::${words[3]}" in
57 plugin::(disable|enable|load))
58 local -aU valid_plugins
60 if [[ "${words[3]}" = disable ]]; then
61 # if command is "disable", only offer already enabled plugins
62 valid_plugins=($plugins)
64 valid_plugins=("$ZSH"/plugins/*/{_*,*.plugin.zsh}(-.N:h:t) "$ZSH_CUSTOM"/plugins/*/{_*,*.plugin.zsh}(-.N:h:t))
65 # if command is "enable", remove already enabled plugins
66 [[ "${words[3]}" = enable ]] && valid_plugins=(${valid_plugins:|plugins})
69 _describe 'plugin' valid_plugins ;;
72 plugins=("$ZSH"/plugins/*/{_*,*.plugin.zsh}(-.N:h:t) "$ZSH_CUSTOM"/plugins/*/{_*,*.plugin.zsh}(-.N:h:t))
73 _describe 'plugin' plugins ;;
76 themes=("$ZSH"/themes/*.zsh-theme(-.N:t:r) "$ZSH_CUSTOM"/**/*.zsh-theme(-.N:r:gs:"$ZSH_CUSTOM"/themes/:::gs:"$ZSH_CUSTOM"/:::))
77 _describe 'theme' themes ;;
79 elif (( CURRENT > 4 )); then
80 case "${words[2]}::${words[3]}" in
81 plugin::(enable|disable|load))
82 local -aU valid_plugins
84 if [[ "${words[3]}" = disable ]]; then
85 # if command is "disable", only offer already enabled plugins
86 valid_plugins=($plugins)
88 valid_plugins=("$ZSH"/plugins/*/{_*,*.plugin.zsh}(-.N:h:t) "$ZSH_CUSTOM"/plugins/*/{_*,*.plugin.zsh}(-.N:h:t))
89 # if command is "enable", remove already enabled plugins
90 [[ "${words[3]}" = enable ]] && valid_plugins=(${valid_plugins:|plugins})
93 # Remove plugins already passed as arguments
94 # NOTE: $(( CURRENT - 1 )) is the last plugin argument completely passed, i.e. that which
95 # has a space after them. This is to avoid removing plugins partially passed, which makes
96 # the completion not add a space after the completed plugin.
98 args=(${words[4,$(( CURRENT - 1))]})
99 valid_plugins=(${valid_plugins:|args})
101 _describe 'plugin' valid_plugins ;;
108 # If run from a script, do not set the completion function
109 if (( ${+functions[compdef]} )); then
115 function _omz::confirm {
116 # If question supplied, ask it before reading the answer
117 # NOTE: uses the logname of the caller function
118 if [[ -n "$1" ]]; then
119 _omz::log prompt "$1" "${${functrace[1]#_}%:*}"
125 # If no newline entered, add a newline
126 if [[ "$REPLY" != $'\n' ]]; then
132 # if promptsubst is set, a message with `` or $()
133 # will be run even if quoted due to `print -P`
134 setopt localoptions nopromptsubst
136 # $1 = info|warn|error|debug
138 # $3 = (optional) name of the logger
141 local logname=${3:-${${functrace[1]#_}%:*}}
143 # Don't print anything if debug is not active
144 if [[ $logtype = debug && -z $_OMZ_DEBUG ]]; then
148 # Choose coloring based on log type
150 prompt) print -Pn "%S%F{blue}$logname%f%s: $2" ;;
151 debug) print -P "%F{white}$logname%f: $2" ;;
152 info) print -P "%F{green}$logname%f: $2" ;;
153 warn) print -P "%S%F{yellow}$logname%f%s: $2" ;;
154 error) print -P "%S%F{red}$logname%f%s: $2" ;;
158 ## User-facing commands
160 function _omz::help {
162 Usage: omz <command> [options]
166 help Print this help message
167 changelog Print the changelog
168 plugin <command> Manage plugins
169 pr <command> Manage Oh My Zsh Pull Requests
170 reload Reload the current zsh session
171 theme <command> Manage themes
172 update Update Oh My Zsh
173 version Show the version
178 function _omz::changelog {
179 local version=${1:-HEAD} format=${3:-"--text"}
183 ! command git show-ref --verify refs/heads/$version && \
184 ! command git show-ref --verify refs/tags/$version && \
185 ! command git rev-parse --verify "${version}^{commit}"
188 Usage: ${(j: :)${(s.::.)0#_}} [version]
190 NOTE: <version> must be a valid branch, tag or commit.
195 "$ZSH/tools/changelog.sh" "$version" "${2:-}" "$format"
198 function _omz::plugin {
199 (( $# > 0 && $+functions[$0::$1] )) || {
201 Usage: ${(j: :)${(s.::.)0#_}} <command> [options]
205 disable <plugin> Disable plugin(s)
206 enable <plugin> Enable plugin(s)
207 info <plugin> Get information of a plugin
208 list List all available Oh My Zsh plugins
209 load <plugin> Load plugin(s)
221 function _omz::plugin::disable {
222 if [[ -z "$1" ]]; then
223 echo >&2 "Usage: ${(j: :)${(s.::.)0#_}} <plugin> [...]"
227 # Check that plugin is in $plugins
229 for plugin in "$@"; do
230 if [[ ${plugins[(Ie)$plugin]} -eq 0 ]]; then
231 _omz::log warn "plugin '$plugin' is not enabled."
234 dis_plugins+=("$plugin")
237 # Exit if there are no enabled plugins to disable
238 if [[ ${#dis_plugins} -eq 0 ]]; then
242 # Remove plugins substitution awk script
243 local awk_subst_plugins="\
244 gsub(/[ \t]+(${(j:|:)dis_plugins})/, \"\") # with spaces before
245 gsub(/(${(j:|:)dis_plugins})[ \t]+/, \"\") # with spaces after
246 gsub(/\((${(j:|:)dis_plugins})\)/, \"\") # without spaces (only plugin)
248 # Disable plugins awk script
250 # if plugins=() is in oneline form, substitute disabled plugins and go to next line
251 /^[ \t]*plugins=\([^#]+\).*\$/ {
257 # if plugins=() is in multiline form, enable multi flag and disable plugins if they're there
258 /^[ \t]*plugins=\(/ {
265 # if multi flag is enabled and we find a valid closing parenthesis, remove plugins and disable multi flag
266 multi == 1 && /^[^#]*\)/ {
273 multi == 1 && length(\$0) > 0 {
275 if (length(\$0) > 0) print \$0
282 local zdot="${ZDOTDIR:-$HOME}"
283 local zshrc="${${:-"${zdot}/.zshrc"}:A}"
284 awk "$awk_script" "$zshrc" > "$zdot/.zshrc.new" \
285 && command cp -f "$zshrc" "$zdot/.zshrc.bck" \
286 && command mv -f "$zdot/.zshrc.new" "$zshrc"
288 # Exit if the new .zshrc file wasn't created correctly
291 _omz::log error "error disabling plugins."
295 # Exit if the new .zshrc file has syntax errors
296 if ! command zsh -n "$zdot/.zshrc"; then
297 _omz::log error "broken syntax in '"${zdot/#$HOME/\~}/.zshrc"'. Rolling back changes..."
298 command mv -f "$zdot/.zshrc.bck" "$zshrc"
302 # Restart the zsh session if there were no errors
303 _omz::log info "plugins disabled: ${(j:, :)dis_plugins}."
305 # Only reload zsh if run in an interactive session
306 [[ ! -o interactive ]] || _omz::reload
309 function _omz::plugin::enable {
310 if [[ -z "$1" ]]; then
311 echo >&2 "Usage: ${(j: :)${(s.::.)0#_}} <plugin> [...]"
315 # Check that plugin is not in $plugins
317 for plugin in "$@"; do
318 if [[ ${plugins[(Ie)$plugin]} -ne 0 ]]; then
319 _omz::log warn "plugin '$plugin' is already enabled."
322 add_plugins+=("$plugin")
325 # Exit if there are no plugins to enable
326 if [[ ${#add_plugins} -eq 0 ]]; then
330 # Enable plugins awk script
332 # if plugins=() is in oneline form, substitute ) with new plugins and go to the next line
333 /^[ \t]*plugins=\([^#]+\).*\$/ {
334 sub(/\)/, \" $add_plugins&\")
339 # if plugins=() is in multiline form, enable multi flag
340 /^[ \t]*plugins=\(/ {
344 # if multi flag is enabled and we find a valid closing parenthesis,
345 # add new plugins and disable multi flag
346 multi == 1 && /^[^#]*\)/ {
348 sub(/\)/, \" $add_plugins&\")
356 local zdot="${ZDOTDIR:-$HOME}"
357 local zshrc="${${:-"${zdot}/.zshrc"}:A}"
358 awk "$awk_script" "$zshrc" > "$zdot/.zshrc.new" \
359 && command cp -f "$zshrc" "$zdot/.zshrc.bck" \
360 && command mv -f "$zdot/.zshrc.new" "$zshrc"
362 # Exit if the new .zshrc file wasn't created correctly
365 _omz::log error "error enabling plugins."
369 # Exit if the new .zshrc file has syntax errors
370 if ! command zsh -n "$zdot/.zshrc"; then
371 _omz::log error "broken syntax in '"${zdot/#$HOME/\~}/.zshrc"'. Rolling back changes..."
372 command mv -f "$zdot/.zshrc.bck" "$zshrc"
376 # Restart the zsh session if there were no errors
377 _omz::log info "plugins enabled: ${(j:, :)add_plugins}."
379 # Only reload zsh if run in an interactive session
380 [[ ! -o interactive ]] || _omz::reload
383 function _omz::plugin::info {
384 if [[ -z "$1" ]]; then
385 echo >&2 "Usage: ${(j: :)${(s.::.)0#_}} <plugin>"
390 for readme in "$ZSH_CUSTOM/plugins/$1/README.md" "$ZSH/plugins/$1/README.md"; do
391 if [[ -f "$readme" ]]; then
392 (( ${+commands[less]} )) && less "$readme" || cat "$readme"
397 if [[ -d "$ZSH_CUSTOM/plugins/$1" || -d "$ZSH/plugins/$1" ]]; then
398 _omz::log error "the '$1' plugin doesn't have a README file"
400 _omz::log error "'$1' plugin not found"
406 function _omz::plugin::list {
407 local -a custom_plugins builtin_plugins
408 custom_plugins=("$ZSH_CUSTOM"/plugins/*(-/N:t))
409 builtin_plugins=("$ZSH"/plugins/*(-/N:t))
411 # If the command is being piped, print all found line by line
412 if [[ ! -t 1 ]]; then
413 print -l ${(q-)custom_plugins} ${(q-)builtin_plugins}
417 if (( ${#custom_plugins} )); then
418 print -P "%U%BCustom plugins%b%u:"
419 print -lac ${(q-)custom_plugins}
422 if (( ${#builtin_plugins} )); then
423 (( ${#custom_plugins} )) && echo # add a line of separation
425 print -P "%U%BBuilt-in plugins%b%u:"
426 print -lac ${(q-)builtin_plugins}
430 function _omz::plugin::load {
431 if [[ -z "$1" ]]; then
432 echo >&2 "Usage: ${(j: :)${(s.::.)0#_}} <plugin> [...]"
436 local plugin base has_completion=0
437 for plugin in "$@"; do
438 if [[ -d "$ZSH_CUSTOM/plugins/$plugin" ]]; then
439 base="$ZSH_CUSTOM/plugins/$plugin"
440 elif [[ -d "$ZSH/plugins/$plugin" ]]; then
441 base="$ZSH/plugins/$plugin"
443 _omz::log warn "plugin '$plugin' not found"
447 # Check if its a valid plugin
448 if [[ ! -f "$base/_$plugin" && ! -f "$base/$plugin.plugin.zsh" ]]; then
449 _omz::log warn "'$plugin' is not a valid plugin"
451 # It it is a valid plugin, add its directory to $fpath unless it is already there
452 elif (( ! ${fpath[(Ie)$base]} )); then
453 fpath=("$base" $fpath)
456 # Check if it has completion to reload compinit
458 comp_files=($base/_*(N))
459 has_completion=$(( $#comp_files > 0 ))
462 if [[ -f "$base/$plugin.plugin.zsh" ]]; then
463 source "$base/$plugin.plugin.zsh"
467 # If we have completion, we need to reload the completion
468 # We pass -D to avoid generating a new dump file, which would overwrite our
469 # current one for the next session (and we don't want that because we're not
470 # actually enabling the plugins for the next session).
471 # Note that we still have to pass -d "$_comp_dumpfile", so that compinit
472 # doesn't use the default zcompdump location (${ZDOTDIR:-$HOME}/.zcompdump).
473 if (( has_completion )); then
474 compinit -D -d "$_comp_dumpfile"
479 (( $# > 0 && $+functions[$0::$1] )) || {
481 Usage: ${(j: :)${(s.::.)0#_}} <command> [options]
485 clean Delete all PR branches (ohmyzsh/pull-*)
486 test <PR_number_or_URL> Fetch PR #NUMBER and rebase against master
498 function _omz::pr::clean {
503 # Check if there are PR branches
505 fmt="%(color:bold blue)%(align:18,right)%(refname:short)%(end)%(color:reset) %(color:dim bold red)%(objectname:short)%(color:reset) %(color:yellow)%(contents:subject)"
506 branches="$(command git for-each-ref --sort=-committerdate --color --format="$fmt" "refs/heads/ohmyzsh/pull-*")"
508 # Exit if there are no PR branches
509 if [[ -z "$branches" ]]; then
510 _omz::log info "there are no Pull Request branches to remove."
514 # Print found PR branches
516 # Confirm before removing the branches
517 _omz::confirm "do you want remove these Pull Request branches? [Y/n] "
518 # Only proceed if the answer is a valid yes option
519 [[ "$REPLY" != [yY$'\n'] ]] && return
521 _omz::log info "removing all Oh My Zsh Pull Request branches..."
522 command git branch --list 'ohmyzsh/pull-*' | while read branch; do
523 command git branch -D "$branch"
528 function _omz::pr::test {
529 # Allow $1 to be a URL to the pull request
530 if [[ "$1" = https://* ]]; then
535 if ! [[ -n "$1" && "$1" =~ ^[[:digit:]]+$ ]]; then
536 echo >&2 "Usage: ${(j: :)${(s.::.)0#_}} <PR_NUMBER_or_URL>"
540 # Save current git HEAD
542 branch=$(builtin cd -q "$ZSH"; git symbolic-ref --short HEAD) || {
543 _omz::log error "error when getting the current git branch. Aborting..."
548 # Fetch PR onto ohmyzsh/pull-<PR_NUMBER> branch and rebase against master
549 # If any of these operations fail, undo the changes made
554 # Get the ohmyzsh git remote
555 command git remote -v | while read remote url _; do
557 https://github.com/ohmyzsh/ohmyzsh(|.git)) found=1; break ;;
558 git@github.com:ohmyzsh/ohmyzsh(|.git)) found=1; break ;;
563 _omz::log error "could not found the ohmyzsh git remote. Aborting..."
567 # Fetch pull request head
568 _omz::log info "fetching PR #$1 to ohmyzsh/pull-$1..."
569 command git fetch -f "$remote" refs/pull/$1/head:ohmyzsh/pull-$1 || {
570 _omz::log error "error when trying to fetch PR #$1."
574 # Rebase pull request branch against the current master
575 _omz::log info "rebasing PR #$1..."
578 # Back up commit.gpgsign setting: use --local to get the current repository
579 # setting, not the global one. If --local is not a known option, it will
580 # exit with a 129 status code.
581 gpgsign=$(command git config --local commit.gpgsign 2>/dev/null) || ret=$?
582 [[ $ret -ne 129 ]] || gpgsign=$(command git config commit.gpgsign 2>/dev/null)
583 command git config commit.gpgsign false
585 command git rebase master ohmyzsh/pull-$1 || {
586 command git rebase --abort &>/dev/null
587 _omz::log warn "could not rebase PR #$1 on top of master."
588 _omz::log warn "you might not see the latest stable changes."
589 _omz::log info "run \`zsh\` to test the changes."
594 "") command git config --unset commit.gpgsign ;;
595 *) command git config commit.gpgsign "$gpgsign" ;;
599 _omz::log info "fetch of PR #${1} successful."
602 # If there was an error, abort running zsh to test the PR
603 [[ $? -eq 0 ]] || return 1
605 # Run zsh to test the changes
606 _omz::log info "running \`zsh\` to test the changes. Run \`exit\` to go back."
609 # After testing, go back to the previous HEAD if the user wants
610 _omz::confirm "do you want to go back to the previous branch? [Y/n] "
611 # Only proceed if the answer is a valid yes option
612 [[ "$REPLY" != [yY$'\n'] ]] && return
618 command git checkout "$branch" -- || {
619 _omz::log error "could not go back to the previous branch ('$branch')."
625 function _omz::reload {
626 # Delete current completion cache
627 command rm -f $_comp_dumpfile $ZSH_COMPDUMP
629 # Old zsh versions don't have ZSH_ARGZERO
630 local zsh="${ZSH_ARGZERO:-${functrace[-1]%:*}}"
631 # Check whether to run a login shell
632 [[ "$zsh" = -* || -o login ]] && exec -l "${zsh#-}" || exec "$zsh"
635 function _omz::theme {
636 (( $# > 0 && $+functions[$0::$1] )) || {
638 Usage: ${(j: :)${(s.::.)0#_}} <command> [options]
642 list List all available Oh My Zsh themes
643 set <theme> Set a theme in your .zshrc file
644 use <theme> Load a theme
656 function _omz::theme::list {
657 local -a custom_themes builtin_themes
658 custom_themes=("$ZSH_CUSTOM"/**/*.zsh-theme(-.N:r:gs:"$ZSH_CUSTOM"/themes/:::gs:"$ZSH_CUSTOM"/:::))
659 builtin_themes=("$ZSH"/themes/*.zsh-theme(-.N:t:r))
661 # If the command is being piped, print all found line by line
662 if [[ ! -t 1 ]]; then
663 print -l ${(q-)custom_themes} ${(q-)builtin_themes}
668 if [[ -n "$ZSH_THEME" ]]; then
669 print -Pn "%U%BCurrent theme%b%u: "
670 [[ $ZSH_THEME = random ]] && echo "$RANDOM_THEME (via random)" || echo "$ZSH_THEME"
674 # Print custom themes if there are any
675 if (( ${#custom_themes} )); then
676 print -P "%U%BCustom themes%b%u:"
677 print -lac ${(q-)custom_themes}
681 # Print built-in themes
682 print -P "%U%BBuilt-in themes%b%u:"
683 print -lac ${(q-)builtin_themes}
686 function _omz::theme::set {
687 if [[ -z "$1" ]]; then
688 echo >&2 "Usage: ${(j: :)${(s.::.)0#_}} <theme>"
692 # Check that theme exists
693 if [[ ! -f "$ZSH_CUSTOM/$1.zsh-theme" ]] \
694 && [[ ! -f "$ZSH_CUSTOM/themes/$1.zsh-theme" ]] \
695 && [[ ! -f "$ZSH/themes/$1.zsh-theme" ]]; then
696 _omz::log error "%B$1%b theme not found"
700 # Enable theme in .zshrc
702 !set && /^[ \t]*ZSH_THEME=[^#]+.*$/ {
704 sub(/^[ \t]*ZSH_THEME=[^#]+.*$/, "ZSH_THEME=\"'$1'\" # set by `omz`")
712 # If no ZSH_THEME= line was found, return an error
717 local zdot="${ZDOTDIR:-$HOME}"
718 local zshrc="${${:-"${zdot}/.zshrc"}:A}"
719 awk "$awk_script" "$zshrc" > "$zdot/.zshrc.new" \
721 # Prepend ZSH_THEME= line to .zshrc if it doesn't exist
723 ZSH_THEME="$1" # set by \`omz\`
727 } > "$zdot/.zshrc.new" \
728 && command cp -f "$zshrc" "$zdot/.zshrc.bck" \
729 && command mv -f "$zdot/.zshrc.new" "$zshrc"
731 # Exit if the new .zshrc file wasn't created correctly
734 _omz::log error "error setting theme."
738 # Exit if the new .zshrc file has syntax errors
739 if ! command zsh -n "$zdot/.zshrc"; then
740 _omz::log error "broken syntax in '"${zdot/#$HOME/\~}/.zshrc"'. Rolling back changes..."
741 command mv -f "$zdot/.zshrc.bck" "$zshrc"
745 # Restart the zsh session if there were no errors
746 _omz::log info "'$1' theme set correctly."
748 # Only reload zsh if run in an interactive session
749 [[ ! -o interactive ]] || _omz::reload
752 function _omz::theme::use {
753 if [[ -z "$1" ]]; then
754 echo >&2 "Usage: ${(j: :)${(s.::.)0#_}} <theme>"
758 # Respect compatibility with old lookup order
759 if [[ -f "$ZSH_CUSTOM/$1.zsh-theme" ]]; then
760 source "$ZSH_CUSTOM/$1.zsh-theme"
761 elif [[ -f "$ZSH_CUSTOM/themes/$1.zsh-theme" ]]; then
762 source "$ZSH_CUSTOM/themes/$1.zsh-theme"
763 elif [[ -f "$ZSH/themes/$1.zsh-theme" ]]; then
764 source "$ZSH/themes/$1.zsh-theme"
766 _omz::log error "%B$1%b theme not found"
770 # Update theme settings
772 [[ $1 = random ]] || unset RANDOM_THEME
775 function _omz::update {
776 local last_commit=$(builtin cd -q "$ZSH"; git rev-parse HEAD)
779 if [[ "$1" != --unattended ]]; then
780 ZSH="$ZSH" command zsh -f "$ZSH/tools/upgrade.sh" --interactive || return $?
782 ZSH="$ZSH" command zsh -f "$ZSH/tools/upgrade.sh" || return $?
785 # Update last updated file
786 zmodload zsh/datetime
787 echo "LAST_EPOCH=$(( EPOCHSECONDS / 60 / 60 / 24 ))" >! "${ZSH_CACHE_DIR}/.zsh-update"
788 # Remove update lock if it exists
789 command rm -rf "$ZSH/log/update.lock"
791 # Restart the zsh session if there were changes
792 if [[ "$1" != --unattended && "$(builtin cd -q "$ZSH"; git rev-parse HEAD)" != "$last_commit" ]]; then
793 # Old zsh versions don't have ZSH_ARGZERO
794 local zsh="${ZSH_ARGZERO:-${functrace[-1]%:*}}"
795 # Check whether to run a login shell
796 [[ "$zsh" = -* || -o login ]] && exec -l "${zsh#-}" || exec "$zsh"
800 function _omz::version {
804 # Get the version name:
805 # 1) try tag-like version
807 # 3) try name-rev (tag~<rev> or branch~<rev>)
809 version=$(command git describe --tags HEAD 2>/dev/null) \
810 || version=$(command git symbolic-ref --quiet --short HEAD 2>/dev/null) \
811 || version=$(command git name-rev --no-undefined --name-only --exclude="remotes/*" HEAD 2>/dev/null) \
812 || version="<detached>"
814 # Get short hash for the current HEAD
815 local commit=$(command git rev-parse --short HEAD 2>/dev/null)
817 # Show version and commit hash
818 printf "%s (%s)\n" "$version" "$commit"