๐ค perhaps you don't need a Zsh plugin manager after all...
TLDR; You don't need a big bloated plugin manager for your Zsh plugins. A simple ~20 line function may be all you need.
Click here to skip to the code.
There are an embarrassingly large number of Zsh plugin managers out there. Many of them are abandonware, are no longer actively developed, are brand new without many users, or don't have much reason to even exist other than as a novelty.
Here's a list of many (but certainly not all) of them from awesome-zsh-plugins:
Zsh Plugin Manager | Performance | Current state |
---|---|---|
antibody | ๐ fast | ๐ฟ Maintenance mode, no new features |
antigen | ๐ข slow | ๐ฟ Maintenance mode, no new features |
antidote | ๐ fast | โ Active |
sheldon | โ unknown | โ Active |
zcomet | ๐ fast | โ Active |
zgem | โ unknown | โ ๏ธ Abandonware |
zgen | ๐ fast | โ ๏ธ Abandonware |
zgenom | ๐ fast | โ Active |
zinit-continuum | ๐ fast | โ Active * |
zinit | ๐ fast | ๐คฌ Author deleted project |
zit | โ unknown | ๐ฟ Few/no recent commits |
znap | ๐ fast | โ Active |
zplug | ๐ข slow | โ ๏ธ Abandonware |
zplugin | ๐ fast | ๐คฌ Renamed to zinit, author deleted |
zpm | ๐ fast | โ Active |
zr | โ unknown | ๐ฟ Few/no recent commits |
Full disclosure, I'm the author of one of these - antidote (formerly called pz).
There's new ones popping up all the time too:
Zsh Plugin Manager | Performance | Current state |
---|---|---|
mzpm | โ unknown | ๐ฃ New |
tzpm | โ unknown | ๐ฃ New |
uz | โ unknown | ๐ฃ New |
zed | โ unknown | ๐ฃ New |
In January 2021, the plugin manager I was using, antibody, was deprecated. The author even went so far as to say:
Most of the other plugin managers catch up on performance, thus keeping this [antibody] does not make sense anymore.
Prior to that, I used zgen, which also stopped being actively developed and the developer seems to have disappeared. (Shoutout to @jandamm for carrying on Zgen with Zgenom!)
In November 2021, a relatively well known and popular Zsh plugin manager, zinit, was removed from GitHub entirely and without warning. In fact, the author deleted almost his entire body of work. Zinit was really popular because it was super fast, and the author promoted his projects in multiple venues for many years. (Shoutout to zdharma-continuum for carrying on with zinit!)
With all the instability in the Zsh plugin manager space, it got me wondering why I even bother with a plugin manager at all.
After antibody was deprecated, I tried znap, but it was in early development at the time and kept breaking, so like many others before me, I decided to write my own - antidote.
When developing antidote, my goal was simple - make a plugin manager that was fast, functional, and easy to understand - which was everything I loved about zgen and antibody. While antidote is a great project, and I fully recommend it if you want to use a plugin manager, I kept wondering if I could cut further down to a single function and see what it would take to not use plugin management utilities altogether.
Thus was born... zsh_unplugged.
This isn't a plugin manager - it's a way to show you how to manage your own plugins using small, easy to understand snippets of Zsh. All this with the thought that perhaps, once-and-for-all, we can demystify what plugin managers do. And maybe for simple configs, do away with the idea that we even need to use bloated Zsh plugin managers and just simply do it ourselves.
You can grab a ~20 line function and you have everything you need to manage your own plugins from here on out. By way of contrast, I ran a rough line count of zinit's codebase which comes out to nearly a whopping ~10,000 lines.
zinit_tmpdir=$(mktemp -d)
git clone --depth 1 https://github.com/zdharma-continuum/zinit $zinit_tmpdir
wc -l $zinit_tmpdir/**/*.zsh
[[ -d $zinit_tmpdir ]] && rm -rf $zinit_tmpdir
Results:
18 /var/folders/z0/w4blz6g14td2lf3b41gmhgd800gn/T/tmp.w2WtwTZJ/docker/init.zsh
61 /var/folders/z0/w4blz6g14td2lf3b41gmhgd800gn/T/tmp.w2WtwTZJ/docker/utils.zsh
186 /var/folders/z0/w4blz6g14td2lf3b41gmhgd800gn/T/tmp.w2WtwTZJ/share/git-process-output.zsh
51 /var/folders/z0/w4blz6g14td2lf3b41gmhgd800gn/T/tmp.w2WtwTZJ/share/rpm2cpio.zsh
19 /var/folders/z0/w4blz6g14td2lf3b41gmhgd800gn/T/tmp.w2WtwTZJ/share/single-line.zsh
23 /var/folders/z0/w4blz6g14td2lf3b41gmhgd800gn/T/tmp.w2WtwTZJ/tests/setup.zsh
12 /var/folders/z0/w4blz6g14td2lf3b41gmhgd800gn/T/tmp.w2WtwTZJ/tests/teardown.zsh
147 /var/folders/z0/w4blz6g14td2lf3b41gmhgd800gn/T/tmp.w2WtwTZJ/zinit-additional.zsh
3486 /var/folders/z0/w4blz6g14td2lf3b41gmhgd800gn/T/tmp.w2WtwTZJ/zinit-autoload.zsh
2389 /var/folders/z0/w4blz6g14td2lf3b41gmhgd800gn/T/tmp.w2WtwTZJ/zinit-install.zsh
404 /var/folders/z0/w4blz6g14td2lf3b41gmhgd800gn/T/tmp.w2WtwTZJ/zinit-side.zsh
3139 /var/folders/z0/w4blz6g14td2lf3b41gmhgd800gn/T/tmp.w2WtwTZJ/zinit.zsh
9935 total
*Note: SLOC is not intended as an indicator of much here beyond a rough comparison of effort, maintainability, and complexity
If you don't want to use anything resembling a plugin manager at all, you could simply clone and source plugins yourself manually:
ZPLUGINDIR=$HOME/.zsh/plugins
if [[ ! -d $ZPLUGINDIR/zsh-autosuggestions ]]; then
git clone https://github.com/zsh-users/zsh-autosuggestions \
$ZPLUGINDIR/zsh-autosuggestions
fi
source $ZPLUGINDIR/zsh-autosuggestions/zsh-autosuggestions.plugin.zsh
if [[ ! -d $ZPLUGINDIR/zsh-history-substring-search ]]; then
git clone https://github.com/zsh-users/zsh-history-substring-search \
$ZPLUGINDIR/zsh-history-substring-search
fi
source $ZPLUGINDIR/zsh-history-substring-search/zsh-history-substring-search.plugin.zsh
if [[ ! -d $ZPLUGINDIR/z ]]; then
git clone https://github.com/rupa/z \
$ZPLUGINDIR/z
fi
source $ZPLUGINDIR/z/z.sh
This can get pretty cumbersome and tricky to maintain. You need to figure out each
plugin's init file, and sometimes adding a plugin to your fpath
is required. While
this method works, there's another way...
If we go one level of abstraction higher than manual git clone
calls, we can use a
simple function wrapper as the basis for everything you need to manage your own Zsh
plugins:
# clone a plugin, identify its init file, source it, and add it to your fpath
function plugin-load {
local repo plugdir initfile
ZPLUGINDIR=${ZPLUGINDIR:-${ZDOTDIR:-$HOME/.config/zsh}/plugins}
for repo in $@; do
plugdir=$ZPLUGINDIR/${repo:t}
initfile=$plugdir/${repo:t}.plugin.zsh
if [[ ! -d $plugdir ]]; then
echo "Cloning $repo..."
git clone -q --depth 1 --recursive --shallow-submodules https://github.com/$repo $plugdir
fi
if [[ ! -e $initfile ]]; then
local -a initfiles=($plugdir/*.plugin.{z,}sh(N) $plugdir/*.{z,}sh{-theme,}(N))
(( $#initfiles )) || { echo >&2 "No init file found '$repo'." && continue }
ln -sf "${initfiles[1]}" "$initfile"
fi
fpath+=$plugdir
(( $+functions[zsh-defer] )) && zsh-defer . $initfile || . $initfile
done
}
That's it. ~20 lines of code and you have a simple, robust Zsh plugin management alternative that is likely as fast as most everything else out there.
What this does is simply clones a Zsh plugin's git repository, and then examines that repo for an appropriate .zsh file to use as an init script. We then find and symlink the plugin's init file if necessary, which allows us to get close to the performance advantage of static sourcing rather than searching for which plugin file to load every time we open a new terminal.
Then, the plugin is sourced and added to fpath
.
You can even get turbocharged-hypersonic-load-speed-magic ๐ if you really need every last bit of performance. See how here.
You are free to grab the plugin-load
function above and put it directly in your
.zshrc, maintain it yourself, and never rely on anyone else's plugin manager again. Or,
this repo makes the plugin-load function available as a plugin itself if you prefer.
Here's an example .zshrc:
# where do you want to store your plugins?
ZPLUGINDIR=${ZPLUGINDIR:-${ZDOTDIR:-$HOME/.config/zsh}/plugins}
# get zsh_unplugged and store it with your other plugins
if [[ ! -d $ZPLUGINDIR/zsh_unplugged ]]; then
git clone --quiet https://github.com/mattmc3/zsh_unplugged $ZPLUGINDIR/zsh_unplugged
fi
source $ZPLUGINDIR/zsh_unplugged/zsh_unplugged.plugin.zsh
# make list of the Zsh plugins you use
repos=(
# plugins that you want loaded first
sindresorhus/pure
# other plugins
zsh-users/zsh-completions
rupa/z
# ...
# plugins you want loaded last
zsh-users/zsh-syntax-highlighting
zsh-users/zsh-history-substring-search
zsh-users/zsh-autosuggestions
)
# now load your plugins
plugin-load $repos
Here is an sample .zshrc.
Updating your plugins is as simple as deleting the $ZPLUGINDIR and reloading Zsh.
ZPLUGINDIR=~/.config/zsh/plugins
rm -rfi $ZPLUGINDIR
zsh
If you are comfortable with git
commands and prefer to not rebuild everything, you
can run git pull
yourself, or even use a simple plugin-update
function:
function plugin-update {
ZPLUGINDIR=${ZPLUGINDIR:-$HOME/.config/zsh/plugins}
for d in $ZPLUGINDIR/*/.git(/); do
echo "Updating ${d:h:t}..."
command git -C "${d:h}" pull --ff --recurse-submodules --depth 1 --rebase --autostash
done
}
You can see what plugins you have installed with a simple ls
command:
ls $ZPLUGINDIR
If you need something fancier and would like to see the git origin of your plugins, you could run this command:
for d in $ZPLUGINDIR/*/.git; do
git -C "${d:h}" remote get-url origin
done
You can just remove it from your plugins
list in your .zshrc. To delete it
altogether, feel free to run rm
:
# remove the fast-syntax-highlighting plugin
rm -rfi $ZPLUGINDIR/fast-syntax-highlighting
You can get turbocharged-hypersonic-load-speed-magic if you choose to use the
romkatv/zsh-defer plugin. Essentially, if you
add romkatv/zsh-defer
to your plugins list, everything you load afterwards will use
zsh-defer, meaning you'll get speeds similar to zinit's turbo mode.
Notably, if you like the zsh-abbr plugin for fish-like abbreviations in Zsh, using zsh-defer will boost performance greatly.
You can separate the clone and load actions into two separate functions, allowing you to further customize how you handle plugins. This technique is especially useful if you are using a project like zsh-utils with nested plugins, or using utilities like zsh-bench which aren't plugins.
# declare a simple plugin-clone function, leaving the user to load plugins themselves
function plugin-clone {
local repo plugdir initfile
ZPLUGINDIR=${ZPLUGINDIR:-${ZDOTDIR:-$HOME/.config/zsh}/plugins}
for repo in $@; do
plugdir=$ZPLUGINDIR/${repo:t}
initfile=$plugdir/${repo:t}.plugin.zsh
if [[ ! -d $plugdir ]]; then
echo "Cloning $repo..."
git clone -q --depth 1 --recursive --shallow-submodules https://github.com/$repo $plugdir
fi
if [[ ! -e $initfile ]]; then
local -a initfiles=($plugdir/*.plugin.{z,}sh(N) $plugdir/*.{z,}sh{-theme,}(N))
(( $#initfiles )) && ln -sf "${initfiles[1]}" "$initfile"
fi
done
}
# now, plugin-source is a separate thing
function plugin-source {
local plugdir
ZPLUGINDIR=${ZPLUGINDIR:-${ZDOTDIR:-$HOME/.config/zsh}/plugins}
for plugdir in $@; do
[[ $plugdir = /* ]] || plugdir=$ZPLUGINDIR/$plugdir
fpath+=$plugdir
local initfile=$plugdir/${plugdir:t}.plugin.zsh
(( $+functions[zsh-defer] )) && zsh-defer . $initfile || . $initfile
done
}
You can then use these two functions like so:
# make a github repo plugins list
repos=(
# not-sourcable plugins
romkatv/zsh-bench
# projects with nested plugins
belak/zsh-utils
ohmyzsh/ohmyzsh
# regular plugins
zsh-users/zsh-autosuggestions
zsh-users/zsh-history-substring-search
zdharma-continuum/fast-syntax-highlighting
)
plugin-clone $repos
# handle non-standard plugins
export PATH="$ZPLUGINDIR/zsh-bench:$PATH"
for file in $ZPLUGINDIR/ohmyzsh/lib/*.zsh; do
source $file
done
# source other plugins
plugins=(
zsh-utils/history
zsh-utils/complete
zsh-utils/utility
ohmyzsh/plugins/magic-enter
ohmyzsh/plugins/history-substring-search
ohmyzsh/plugins/z
fast-syntax-highlighting
zsh-autosuggestions
)
plugin-source $plugins
Here is an sample .zshrc.
If you are an experienced Zsh user, you may know about zcompile, which takes your Zsh scripts and potentially speeds them up by compiling them to byte code. If you feel confident you know what you're doing and want to eek every last bit of performance out of your Zsh, you can use this function:
function plugin-compile {
ZPLUGINDIR=${ZPLUGINDIR:-$HOME/.config/zsh/plugins}
autoload -U zrecompile
local f
for f in $ZPLUGINDIR/**/*.zsh{,-theme}(N); do
zrecompile -pq "$f"
done
}
Oh-My-Zsh and Prezto have their own built-in methods for loading plugins, they just don't come with a way to clone them. You don't need the zsh_unplugged script if you are using those frameworks. However, you also don't need a separate plugin manager utility. Here's how you handle cloning yourself and go plugin-manager-free with Zsh frameworks:
If you are using Oh-My-Zsh, the way to go without a plugin manager would be
to utilize the $ZSH_CUSTOM
path.
Note that this assumes your init file is called {plugin_name}.plugin.zsh which may not be true.
# .zshrc
# don't call this list 'plugins' since omz uses that
repos=(
marlonrichert/zsh-hist
zsh-users/zsh-syntax-highlighting
zsh-users/zsh-autosuggestions
)
for repo in $repos; do
if [[ ! -d $ZSH_CUSTOM/${repo:t} ]]; then
git clone https://github.com/${repo} $ZSH_CUSTOM/plugins/${repo:t}
fi
done
unset repo{s,}
# add your external plugins to your OMZ plugins list
plugins=(
...
zsh-hist
zsh-autosuggestions
...
zsh-syntax-highlighting
)
If you are using Prezto, the way to go without a plugin manager would be to
utilize the $ZPREZTODIR/contrib
path.
Note that this assumes your init file is called {plugin_name}.plugin.zsh which may not be true.
# .zshrc
contribs=(
rupa/z
marlonrichert/zsh-hist
mattmc3/zman
)
for contrib in $contribs; do
if [[ ! -d $ZPREZTODIR/contrib/${contrib:t} ]]; then
git clone https://github.com/${contrib} $ZPREZTODIR/contrib/${contrib:t}
fi
done
unset contrib{,s}
# add the contribs to your Prezto modules list in your `.zpreztorc`
zstyle ':prezto:load' pmodule \
... \
z \
zsh-hist \
... \
zman