ℹ️ Zsh Plugin Standard
What is a Zsh plugin?
从历史上看,Zsh 插件最初是由 Oh My Zsh 定义的。 They provide a way to package together files that extend or configure the shell’s functionality in a particular way.
简单来说,一个插件:
-
Has directory added to
$fpath
(Zsh documentation: #Autoloading-Functions). This is being done either by a plugin manager or by the plugin itself (see 5th section for more information). -
Has first
*.plugin.zsh
file sourced (or*.zsh
,init.zsh
,*.sh
, these are non-standard).2.1 The first point allows plugins to provide completions and functions that are loaded via Zsh’s
autoload
mechanism (a single function per file). -
从更全面的角度来看,一个插件包含以下几点:
3.1. a directory containing various files (the main script, autoload functions, completions, Makefiles, backend programs, documentation).
3.2. a source-able script that obtains the path to its directory via
$0
(see the next section for a related enhancement proposal).3.3. GitHub (or another site) repository identified by two components username/plugin-name.
3.4. software package containing any type of command line artifacts – when used with advanced plugin managers that have hooks, can run Makefiles, and add directories to
$PATH
.
Below are the proposed enhancements and codifications of the definition of a "Zsh the plugin" and the actions of plugin managers – the proposed standardization.
它们涵盖了如何编写Zsh插件的信息。
1. Standardized $0
handling
[ zero-handling ]
要获取插件的位置,插件应该这样做:
0="${ZERO:-${${0:#$ZSH_ARGZERO}:-${(%):-%N}}}"
0="${${(M)0:#/*}:-$PWD/$0}"
Then ${0:h}
to get the plugin’s directory.
以上的单行代码将:
-
Be backward-compatible with normal
$0
setting and usage. -
Use
ZERO
if it’s not empty,2.1. the plugin manager will be easily able to alter effective
$0
before loading a plugin,2.2. this allows e.g.
eval "$(<plugin)"
, which can be faster thansource
(comparison note that it’s not for a compiled script). -
Use
$0
if it doesn’t contain the path to the Zsh binary,3.1. plugin manager will still be able to set
$0
, although more difficult, requiresunsetopt function_argzero
before sourcing the plugin script, and0=…
assignment after sourcing the plugin script.3.2.
unsetopt function_argzero
will be detected (it causes$0
not to contain a plugin-script path, but the path to Zsh binary, if not overwritten by a0=…
assignment),3.3.
setopt posix_argzero
will be detected (as above). -
Use the
%N
prompt expansion flag, which always gives the absolute path to the script,4.1. plugin manager cannot alter this (no advanced loading of the plugin is possible), but simple plugin-file sourcing (without a plugin manager) will be saved from breaking caused by the mentioned
*_argzero
options, so this is a very good last-resort fallback. -
Finally, in the second line, it will ensure that
$0
contains an absolute path by prepending it with$PWD
if necessary.
The goal is flexibility, with essential motivation to support eval "$(<plugin)"
and solve setopt no_function_argzero
and setopt posix_argzero
cases.
A plugin manager will be even able to convert a plugin to a function, but the performance differences of this are yet unclear.
但是,它可能会提供一个用例。
The last, 5th point also allows using the $0
handling in scripts (i.e. runnable with the hashbang #!…
) to get the directory in which the script file resides.
The assignment uses quoting to make it resilient to the combination of the GLOB_SUBST
and GLOB_ASSIGN
options. It’s a standard snippet of code, so it has to be always working.
When you set e.g.: the zsh
emulation in a function, you in general don’t have to quote assignments.
STATUS: [ zero-handling ]
- GitHub Search: ZSH_ARGZERO
2. Functions directory
[ functions-directory ]
Despite that, the current-standard plugins have their main directory added to $fpath
, a more clean approach is being proposed: that the plugins use a subdirectory called functions
to store their completions and autoload functions. This will allow a much cleaner design of plugins. The plugin manager should add such a directory to $fpath
. The lack of support of the current plugin managers can be easily resolved via the indicator:
if [[ ${zsh_loaded_plugins[-1]} != */kalc && -z ${fpath[(r)${0:h}/functions]} ]]; then
fpath+=( "${0:h}/functions" )
fi
or, via the use of the PMSPEC
parameter:
if [[ $PMSPEC != *f* ]]; then
fpath+=( "${0:h}/functions" )
fi
The above snippet added to the plugin.zsh
file will add the directory to the $fpath
with the compatibility with any new plugin managers preserved. The existence of the functions
subdirectory cancels the normal adding of the main plugin directory to $fpath
.
STATUS: [ functions-directory ]
- GitHub Search: zsh_loaded_plugins
- GitHub Search: PMSPEC *f*
3. Binaries directory
[ binaries-directory ]
Plugins sometimes provide a runnable script or program, either for their internal use or for the end-user. It is proposed that for the latter, the plugin shall use a bin/
subdirectory inside its main dir (it is recommended, that for internal use, the runnable be called via the $0
value obtained as described above).
The runnable should be put into the directory with a +x
access right assigned. The task of the plugin manager should be:
-
Before sourcing the plugin’s script it should test, if the
bin/
directory exists within the plugin directory. -
If it does, it should add the directory to
$PATH
. -
The plugin manager can also, instead of extending the
$PATH
, create a shim (i.e.: a forwarder script) or a symbolic link inside a common directory that’s already added to$PATH
(to limit extending it). -
The plugin manager is permitted to do optional things like ensuring
+x
access rights on the directory contents. The$PMSPEC
code letter for the feature isb
, and it allows for the plugin to handle the$PATH
extending itself, via, e.g.:
if [[ $PMSPEC != *b* ]]; then
path+=( "${0:h}/bin" )
fi
STATUS: [ binaries-directory ]
- GitHub Search: PMSPEC *b*
4. Unload function
[ unload-function ]
If a plugin is named e.g. kalc
(and is available via any-user/kalc
plugin-ID), then it can provide a function, kalc_plugin_unload
, that can be called by a plugin manager to undo the effects of loading that plugin.
A plugin manager can implement its tracking of changes made by a plugin so this is in general optional. However, to properly unload e.g. a prompt, dedicated tracking (easy to do for the plugin creator) can provide better, predictable results. Any special, uncommon effects of loading a plugin are possible to undo only by a dedicated function.
However, an interesting compromise approach is available – to withdraw only the special effects of loading a plugin via the dedicated, plugin-provided function and leave the rest to the plugin manager. The value of such an approach is that maintaining such function (if it is to withdraw all plugin side-effects) can be a daunting task requiring constant monitoring during the plugin development process.
Note that the unload function should contain unfunction $0
(or better unfunction kalc_plugin_unload
etc., for compatibility with the *_argzero
options), to also delete the function itself.
STATUS: [ unload-function ]
-
Zi implements plugin unloading and calls the function.
-
romkatv/powerlevel10k is using the function to execute a specific task: shutdown of the binary, background gitstatus daemon, with very good results,
-
agkozak/agkozak-zsh-prompt is using the function to completely unload the prompt,
-
agkozak/zsh-z is using the function to completely unload the plugin,
-
agkozak/zhooks is using the function to completely unload the plugin.
5. @zsh-plugin-run-on-unload
call
[ run-on-unload-call ]
The plugin manager can provide a function @zsh-plugin-run-on-unload
which has the following call syntax:
@zsh-plugin-run-on-unload "{code-snippet-1}" "{code-snippet-2}" …
The function registers pieces of code to be run by the plugin manager on the unloading of the plugin. The execution of the code should be done by the eval
built-in in the same order as they are passed to the call. The code should be executed in the plugin’s directory, in the current shell. The mechanism thus provides another way, side to the unload function, for the plugin to participate in the process of unloading it.
STATUS: [ run-on-unload-call ]
- GitHub Search: zsh-plugin-run-on-unload
6. @zsh-plugin-run-on-update
call
[ run-on-update-call ]
The plugin manager can provide a function @zsh-plugin-run-on-update
which has the following call syntax:
@zsh-plugin-run-on-update "{code-snippet-1}" "{code-snippet-2}" …
The function registers pieces of code to be run by the plugin manager on an update of the plugin. The execution of the code should be done by the eval
built-in in the same order as they are passed to the call. The code should be executed in the plugin’s directory, possibly in a subshell After downloading any new commits to the repository.
STATUS: [ run-on-update-call ]
- GitHub Search: zsh-plugin-run-on-update
7. Plugin manager activity indicator
[ activity-indicator ]
Plugin managers should set the $zsh_loaded_plugins
array to contain all previously loaded plugins and the plugin currently being loaded (as the last element).
This will allow any plugin to:
-
Check which plugins are already loaded.
-
Check if it is being loaded by a plugin manager (i.e. not just sourced).
The first item allows a plugin to e.g. issue a notice about missing dependencies. Instead of issuing a notice, it may be able to satisfy the dependencies on resources it provides. For example, the pure
prompt provides a zsh-async
dependency library within its source tree, which is normally a separate project. Consequently, the prompt can decide to source its private copy of zsh-async
, having also reliable $0
defined by the previous section (note: pure
doesn’t normally do this).
The second item allows a plugin to e.g. set up $fpath
, knowing that the plugin manager will not handle this:
if [[ ${zsh_loaded_plugins[-1]} != */kalc && -z ${fpath[(r)${0:h}]} ]]; then
fpath+=( "${0:h}" )
fi
This will allow the user to reliably source the plugin without using a plugin manager. The code uses the wrapping braces around variables (i.e.: e.g.: ${fpath…}
) to make it compatible with the KSH_ARRAYS
option and the quoting around ${0:h}
to make it compatible with the SH_WORD_SPLIT
option.
STATUS: [ activity-indicator ]
- GitHub Search: zsh_loaded_plugins
8. Global parameter with PREFIX for make, configure, etc
[ global-parameter-with-prefix ]
Plugin managers may export the parameter $ZPFX
which should contain a path to a directory dedicated to user-land software, i.e. for directories $ZPFX/bin
, $ZPFX/lib
, $ZPFX/share
, etc. The suggested name of the directory is polaris
(e.g.: Zi uses this name and places this directory at ~/.zi/polaris
by default).
Users can then configure hooks to invoke e.g. make PREFIX=$ZPFX install
at clone & update the plugin to install software like e.g. tj/git-extras. This is the developing role of Zsh plugin managers as package managers, where .zshrc
has a similar role to Chef or Puppet configuration and allows to declare system state and have the same state on different accounts/machines.
No-narration facts-list related to $ZPFX
:
-
export ZPFX="$HOME/polaris"
(or e.g.$HOME/.zi/polaris
) -
make PREFIX=$ZPFX install
-
./configure --prefix=$ZPFX
-
cmake -DCMAKE_INSTALL_PREFIX=$ZPFX .
-
zi ice make"PREFIX=$ZPFX install"
-
zi … hook-build:"make PREFIX=$PFX install"
STATUS: [ global-parameter-with-prefix ]
- GitHub Search: ZPFX
9. Global parameter holding the plugin manager’s capabilities
[ global-parameter-with-capabilities ]
The above paragraphs of the standard spec each constitute a capability, a feature of the plugin manager. It would make sense that the capabilities are somehow discoverable. To address this, a global parameter called PMSPEC
(from plugin-manager specification) is proposed. It can hold the following Latin letters each informing the plugin, that the plugin manager has support for a given feature:
-
0
– the plugin manager provides theZERO
parameter, -
f
- … supports thefunctions/
subdirectory, -
b
- … supports thebin/
subdirectory, -
u
- … the unload function, -
U
- … the@zsh-plugin-run-on-unload
call, -
p
– … the@zsh-plugin-run-on-update
call, -
i
– … thezsh_loaded_plugins
activity indicator, -
b
– … theZPFX
global parameter, -
s
– … thePMSPEC
global parameter itself (i.e.: should be always present).
The contents of the parameter describing a fully compliant plugin manager should be: 0fuUpiPs
.
The plugin can then verify the support by:
if [[ $PMSPEC != *P* ]]; then
path+=( "${0:h}/bin" )
fi
STATUS: [ global-parameter-with-capabilities ]
- GitHub Search: PMSPEC
Zsh plugin-programming best practices
The document is to define a Zsh-plugin but also to serve as an information source for plugin creators. Therefore, it covers also best practices information in this section.
Use of add-zsh-hook
to install hooks
Zsh ships with the function add-zsh-hook
. It has the following invocation syntax:
add-zsh-hook [ -L | -dD ] [ -Uzk ] hook function
The command installs a function
as one of the supported zsh hook
entries: chpwd
, periodic
, precmd
, preexec
, zshaddhistory
, zshexit
, and zsh_directory_name
. For their meaning refer to the Zsh documentation: #Hook-Functions.
Use of add-zle-hook-widget
to install Zle Hooks
The Zle editor is the part of the Zsh that is responsible for receiving the text from the user. It can be said that it’s based on widgets, which are nothing more than Zsh functions that are allowed to be run in Zle context, i.e. from the Zle editor (plus a few minor differences, like e.g.: the $WIDGET
parameter that’s automatically set by the Zle editor).
The syntax of the call is:
add-zle-hook-widget [ -L | -dD ] [ -Uzk ] hook widget_name
The call resembles the syntax of the add-zsh-hook
function. The only difference is that it takes a widget_name
, not a function name, and that the hook
is one of: isearch-exit
, isearch-update
, line-pre-redraw
, line-init
, line-finish
, history-line-set
, or keymap-select
. Their meaning is explained in the Zsh documentation: #Special-Widgets.
The use of this function is recommended because it allows the installation multiple hooks per each hook
entry. Before introducing the add-zle-hook-widget
function the "normal" way to install a hook was to define a widget with the name of one of the special widgets. Now, after the function has been introduced in Zsh 5.3
it should be used instead.
Standard parameter naming
There’s a convention already present in the Zsh world – to name array variables in lowercase and scalars uppercase. It’s being followed by e.g.: the Zsh manual and the Z shell itself (e.g.: REPLY
scalar and reply
array, etc.).
The requirement for the scalars to be uppercase should be, in my opinion, kept only for the global parameters. e.g.: it’s fine to name local parameters inside a function lowercase even when they are scalars, not only arrays.
An extension to the convention is being proposed: to name associative arrays (i.e.: hashes) capitalized, i.e.: with only the first letter uppercase and the remaining letters lowercase.
See the next section for an example of such hash. In the case of the name consisting of multiple words each of them should be capitalized, e.g.: typeset -A MyHash
.
This convention will increase code readability and bring order to it.
Standard Plugins
hash
The plugin often has to declare global parameters that should live throughout a Zsh session. Following the namespace pollution prevention the plugin could use a hash to store the different values. Additionally, the plugins could use a single hash parameter – called Plugins
– to prevent pollution.
An example value needed by the plugin:
typeset -gA Plugins
Plugins[MY_PLUGIN_REPO_DIR]="${0:h}"
This way all the data of all plugins will be kept in a single parameter, available for easy examination and overview (via e.g.: varied Plugins
), and also not polluting the namespace.
Standard recommended options
The following code snippet is recommended to be included at the beginning of each of the main functions provided by the plugin:
builtin emulate -L zsh ${=${options[xtrace]:#off}:+-o xtrace}
builtin setopt extended_glob warn_create_global typeset_silent no_short_loops rc_quotes no_auto_pushd
The emulate -LR zsh
can be used to emulate a clean Zsh shell environment that is local to the function it's called from. The -L
and -R
are emulation mode switches:
-
-L
: This flag makes the emulation local to the function it's called from. This means that the emulation will not affect the shell environment outside of the function. -
-R
: This flag resets all options to their default values as if the shell had just been started. This is useful when you want to ensure a clean, predictable environment.
Reference: zsh shell builtin commands
The emulation is altered with the following options:
-
${=${options[xtrace]:#off}:+-o xtrace}
–xtrace
prints commands and their arguments as they are executed, this specific variable callsxtrace
when needed, e.g.: when already active at the entry to the function. -
extended_glob
– enables one of the main Zshell features – the advanced, built-in regex-like globing mechanism, -
warn_create_global
– enables warnings to be printed each time a (global) variable is defined without being explicitly defined by atypeset
,local
,declare
, etc. call; it allows to catch typos and missing localizations of the variables and thus prevent from writing a bad code, -
typeset_silent
– it allows to calltypeset
,local
, etc. multiple times on the same variable; without it, the second call causes the variable contents to be printed first; using this option allows declaring variables inside loops, near the place of their use, which sometimes helps to write a more readable code, -
no_short_loops
– disables the short-loops syntax; this is done because when the syntax is enabled it limits the parser’s ability to detect errors (see this zsh-workers post for the details), -
rc_quotes
– adds the useful ability to insert apostrophes into an apostrophe-quoted string, by use of''
inside it, e.g.:'a string’s example'
will yield the string `a string’s example, -
no_auto_pushd
- disables the automatic push of the directory passed tocd
builtin onto the directory stack; this is useful because otherwise, the internal directory changes done by the plugin will pollute the global directory stack.
Standard recommended variables
It’s good to localize the following variables at the entry of the main function of a plugin:
local MATCH REPLY; integer MBEGIN MEND
local -a match mbegin mend reply