On , I learnt ...

which behaves diffferently in Zsh compared to Bash

which is a command used to identify the location of executables; however it behaves differently on Zsh compared to Bash.

In Bash, which is an executable file, normally found in /usr/bin/which. It has a man page like other commands.

In Zsh, which is an alias of the whence builtin. From man zshbuiltins:

whence [ -vcwfpamsS ] [ -x num ] name ...
       For each name, indicate how it would be interpreted if used as a
       command name.

       If name is not an alias, built-in command, external command, shell
       function, hashed command, or a reserved word, the exit status
       shall be non-zero, and -- if -v, -c, or -w was passed -- a message
       will be written to standard output.  (This is different from other
       shells that write that message to standard error.)

       ...

       -c     Print the results in a csh-like format.  This takes
              precedence over -v.
       ...

which [ -wpamsS ] [ -x num ] name ...
       Equivalent to whence -c.

What difference does it make?

A notable difference is that, in Zsh, if the passed name is a shell function, which will print its implementation.

Pyenv example

This is apparent with pyenv which declares a shell function called pyenv when its init function is called:

eval "$(pyenv init -)"

In Bash, which shows the location where Homebrew has installed it:

$ which pyenv
/usr/local/bin/pyenv

But in Zsh we see its function implementation:

pyenv () {
	local command
	command="${1:-}"
	if [ "$#" -gt 0 ]
	then
		shift
	fi
	case "$command" in
		(rehash | shell | virtualenvwrapper | virtualenvwrapper_lazy) eval "$(pyenv "sh-$command" "$@")" ;;
		(*) command pyenv "$command" "$@" ;;
	esac
}

This caught us out in trying to write Python upgrade instructions that worked in both Bash and Zsh.