Compare commits

..
Author SHA1 Message Date
Robby RussellandClaude Fable 5.1 de76990950 fix(cli): address review feedback on omz generate plugin
- Turn %placeholders% into private markers when a template is read and
  strip the marker from every value, so text inserted from user input
  (e.g. -d '%name%') is never rescanned as a placeholder.
- When --enable is used for a name already in $plugins (a custom
  override of an enabled built-in), report it and reload instead of
  failing in _omz::plugin::enable.
- Make the completion note accurate whether or not a _<cmd> file was
  generated.
- Test the write-phase cleanup with a template set missing a later
  template, and test that placeholder-looking values come out literally.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 14:48:54 -07:00
Robby RussellandClaude Fable 5.1 46aee25f27 feat(cli): add omz generate plugin to scaffold custom plugins
Adds a rails-style generator that creates a custom plugin in
$ZSH_CUSTOM/plugins/<name> from commented templates: the plugin file,
a README in the usual shape and, for plugins that wrap a command-line
tool, a completion skeleton (or the cached-completion block when the
tool generates its own). Anything not passed as an option is asked
for interactively; non-interactive shells use defaults.

`omz generate` is a new top-level command so that other generators
(e.g. `omz generate theme`) can follow the same shape.

Templates live in templates/generators/plugin and are syntax-checked
by CI. lib/tests/generate-plugin.test.zsh covers the non-interactive
path, name validation and the prompt helper.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 14:28:32 -07:00
20 changed files with 886 additions and 168 deletions
+1 -2
View File
@@ -36,10 +36,9 @@ jobs:
run: |
for file in ./oh-my-zsh.sh \
./lib/*.zsh \
./functions/* \
./completions/_* \
./plugins/*/*.plugin.zsh \
./plugins/*/_* \
./templates/generators/*/*.zsh-template \
./themes/*.zsh-theme; do
zsh -n "$file" || return 1
done
+4
View File
@@ -346,6 +346,10 @@ directory.
If you have many functions that go well together, you can put them as a `XYZ.plugin.zsh` file in the
`custom/plugins/` directory and then enable this plugin.
The quickest way to start one is `omz generate plugin <name>`. It creates the plugin directory with a
commented `<name>.plugin.zsh`, a `README.md`, and (for plugins that wrap a command-line tool) a completion
skeleton, then tells you how to try it out.
If you would like to override the functionality of a plugin distributed with Oh My Zsh, create a plugin of the
same name in the `custom/plugins/` directory and it will be loaded instead of the one in `plugins/`.
-90
View File
@@ -1,90 +0,0 @@
#compdef omz
local -a cmds subcmds
cmds=(
'changelog:Print the changelog'
'help:Usage information'
'plugin:Manage plugins'
'pr:Manage Oh My Zsh Pull Requests'
'reload:Reload the current zsh session'
'shop:Open the Oh My Zsh shop'
'theme:Manage themes'
'update:Update Oh My Zsh'
'version:Show the version'
)
if (( CURRENT == 2 )); then
_describe 'command' cmds
elif (( CURRENT == 3 )); then
case "$words[2]" in
changelog) local -a refs
refs=("${(@f)$(builtin cd -q "$ZSH"; command git for-each-ref --format="%(refname:short):%(subject)" refs/heads refs/tags)}")
_describe 'command' refs ;;
plugin) subcmds=(
'disable:Disable plugin(s)'
'enable:Enable plugin(s)'
'info:Get plugin information'
'list:List plugins'
'load:Load plugin(s)'
)
_describe 'command' subcmds ;;
pr) subcmds=('clean:Delete all Pull Request branches' 'test:Test a Pull Request')
_describe 'command' subcmds ;;
theme) subcmds=('list:List themes' 'set:Set a theme in your .zshrc file' 'use:Load a theme')
_describe 'command' subcmds ;;
esac
elif (( CURRENT == 4 )); then
case "${words[2]}::${words[3]}" in
plugin::(disable|enable|load))
local -aU valid_plugins
if [[ "${words[3]}" = disable ]]; then
# if command is "disable", only offer already enabled plugins
valid_plugins=($plugins)
else
valid_plugins=("$ZSH"/plugins/*/{_*,*.plugin.zsh}(-.N:h:t) "$ZSH_CUSTOM"/plugins/*/{_*,*.plugin.zsh}(-.N:h:t))
# if command is "enable", remove already enabled plugins
[[ "${words[3]}" = enable ]] && valid_plugins=(${valid_plugins:|plugins})
fi
_describe 'plugin' valid_plugins ;;
plugin::info)
local -aU plugins
plugins=("$ZSH"/plugins/*/{_*,*.plugin.zsh}(-.N:h:t) "$ZSH_CUSTOM"/plugins/*/{_*,*.plugin.zsh}(-.N:h:t))
_describe 'plugin' plugins ;;
plugin::list)
local -a opts
opts=('--enabled:List enabled plugins only')
_describe -o 'options' opts ;;
theme::(set|use))
local -aU themes
themes=("$ZSH"/themes/*.zsh-theme(-.N:t:r) "$ZSH_CUSTOM"/**/*.zsh-theme(-.N:r:gs:"$ZSH_CUSTOM"/themes/:::gs:"$ZSH_CUSTOM"/:::))
_describe 'theme' themes ;;
esac
elif (( CURRENT > 4 )); then
case "${words[2]}::${words[3]}" in
plugin::(enable|disable|load))
local -aU valid_plugins
if [[ "${words[3]}" = disable ]]; then
# if command is "disable", only offer already enabled plugins
valid_plugins=($plugins)
else
valid_plugins=("$ZSH"/plugins/*/{_*,*.plugin.zsh}(-.N:h:t) "$ZSH_CUSTOM"/plugins/*/{_*,*.plugin.zsh}(-.N:h:t))
# if command is "enable", remove already enabled plugins
[[ "${words[3]}" = enable ]] && valid_plugins=(${valid_plugins:|plugins})
fi
# Remove plugins already passed as arguments
# NOTE: $(( CURRENT - 1 )) is the last plugin argument completely passed, i.e. that which
# has a space after them. This is to avoid removing plugins partially passed, which makes
# the completion not add a space after the completed plugin.
local -a args
args=(${words[4,$(( CURRENT - 1))]})
valid_plugins=(${valid_plugins:|args})
_describe 'plugin' valid_plugins ;;
esac
fi
return 0
+480 -11
View File
@@ -1,3 +1,5 @@
#!/usr/bin/env zsh
function omz {
setopt localoptions noksharrays
[[ $# -gt 0 ]] || {
@@ -18,6 +20,109 @@ function omz {
_omz::$command "$@"
}
function _omz {
local -a cmds subcmds
cmds=(
'changelog:Print the changelog'
'generate:Run a generator, e.g. to create a custom plugin'
'help:Usage information'
'plugin:Manage plugins'
'pr:Manage Oh My Zsh Pull Requests'
'reload:Reload the current zsh session'
'shop:Open the Oh My Zsh shop'
'theme:Manage themes'
'update:Update Oh My Zsh'
'version:Show the version'
)
if (( CURRENT == 2 )); then
_describe 'command' cmds
elif (( CURRENT == 3 )); then
case "$words[2]" in
changelog) local -a refs
refs=("${(@f)$(builtin cd -q "$ZSH"; command git for-each-ref --format="%(refname:short):%(subject)" refs/heads refs/tags)}")
_describe 'command' refs ;;
generate) subcmds=('plugin:Create a custom plugin')
_describe 'generator' subcmds ;;
plugin) subcmds=(
'disable:Disable plugin(s)'
'enable:Enable plugin(s)'
'info:Get plugin information'
'list:List plugins'
'load:Load plugin(s)'
)
_describe 'command' subcmds ;;
pr) subcmds=('clean:Delete all Pull Request branches' 'test:Test a Pull Request')
_describe 'command' subcmds ;;
theme) subcmds=('list:List themes' 'set:Set a theme in your .zshrc file' 'use:Load a theme')
_describe 'command' subcmds ;;
esac
elif (( CURRENT == 4 )); then
case "${words[2]}::${words[3]}" in
generate::plugin)
_omz::generate::plugin::complete_options ;;
plugin::(disable|enable|load))
local -aU valid_plugins
if [[ "${words[3]}" = disable ]]; then
# if command is "disable", only offer already enabled plugins
valid_plugins=($plugins)
else
valid_plugins=("$ZSH"/plugins/*/{_*,*.plugin.zsh}(-.N:h:t) "$ZSH_CUSTOM"/plugins/*/{_*,*.plugin.zsh}(-.N:h:t))
# if command is "enable", remove already enabled plugins
[[ "${words[3]}" = enable ]] && valid_plugins=(${valid_plugins:|plugins})
fi
_describe 'plugin' valid_plugins ;;
plugin::info)
local -aU plugins
plugins=("$ZSH"/plugins/*/{_*,*.plugin.zsh}(-.N:h:t) "$ZSH_CUSTOM"/plugins/*/{_*,*.plugin.zsh}(-.N:h:t))
_describe 'plugin' plugins ;;
plugin::list)
local -a opts
opts=('--enabled:List enabled plugins only')
_describe -o 'options' opts ;;
theme::(set|use))
local -aU themes
themes=("$ZSH"/themes/*.zsh-theme(-.N:t:r) "$ZSH_CUSTOM"/**/*.zsh-theme(-.N:r:gs:"$ZSH_CUSTOM"/themes/:::gs:"$ZSH_CUSTOM"/:::))
_describe 'theme' themes ;;
esac
elif (( CURRENT > 4 )); then
case "${words[2]}::${words[3]}" in
generate::plugin)
_omz::generate::plugin::complete_options ;;
plugin::(enable|disable|load))
local -aU valid_plugins
if [[ "${words[3]}" = disable ]]; then
# if command is "disable", only offer already enabled plugins
valid_plugins=($plugins)
else
valid_plugins=("$ZSH"/plugins/*/{_*,*.plugin.zsh}(-.N:h:t) "$ZSH_CUSTOM"/plugins/*/{_*,*.plugin.zsh}(-.N:h:t))
# if command is "enable", remove already enabled plugins
[[ "${words[3]}" = enable ]] && valid_plugins=(${valid_plugins:|plugins})
fi
# Remove plugins already passed as arguments
# NOTE: $(( CURRENT - 1 )) is the last plugin argument completely passed, i.e. that which
# has a space after them. This is to avoid removing plugins partially passed, which makes
# the completion not add a space after the completed plugin.
local -a args
args=(${words[4,$(( CURRENT - 1))]})
valid_plugins=(${valid_plugins:|args})
_describe 'plugin' valid_plugins ;;
esac
fi
return 0
}
# If run from a script, do not set the completion function
if (( ${+functions[compdef]} )); then
compdef _omz omz
fi
## Utility functions
function _omz::confirm {
@@ -71,15 +176,16 @@ Usage: omz <command> [options]
Available commands:
help Print this help message
changelog Print the changelog
plugin <command> Manage plugins
pr <command> Manage Oh My Zsh Pull Requests
reload Reload the current zsh session
shop Open the Oh My Zsh shop
theme <command> Manage themes
update Update Oh My Zsh
version Show the version
help Print this help message
changelog Print the changelog
generate <generator> Run a generator, e.g. to create a custom plugin
plugin <command> Manage plugins
pr <command> Manage Oh My Zsh Pull Requests
reload Reload the current zsh session
shop Open the Oh My Zsh shop
theme <command> Manage themes
update Update Oh My Zsh
version Show the version
EOF
}
@@ -104,6 +210,371 @@ EOF
ZSH="$ZSH" command zsh -f "$ZSH/tools/changelog.sh" "$version" "${2:-}" "$format"
}
function _omz::generate {
(( $# > 0 && $+functions[$0::$1] )) || {
cat >&2 <<EOF
Usage: ${(j: :)${(s.::.)0#_}} <generator> [options]
Available generators:
plugin [<name>] Create a custom plugin in \$ZSH_CUSTOM/plugins
EOF
return 1
}
local command="$1"
shift
$0::$command "$@"
}
function _omz::generate::plugin {
setopt localoptions extendedglob
local -A opts
zparseopts -D -E -A opts -- d: -description: c: -command: -no-completion -enable y -yes h -help || return 1
if (( ${+opts[-h]} || ${+opts[--help]} )); then
_omz::generate::plugin::usage
return 0
fi
# zparseopts -E leaves anything it doesn't recognise in $@
local arg
for arg in "$@"; do
if [[ "$arg" == -* ]]; then
_omz::log error "unknown option '$arg'."
_omz::generate::plugin::usage
return 1
fi
done
if (( $# > 1 )); then
_omz::generate::plugin::usage
return 1
fi
# Only ask questions when there is someone there to answer them
local interactive=0
if [[ -o interactive && -t 0 ]] && (( ! ${+opts[-y]} && ! ${+opts[--yes]} )); then
interactive=1
fi
local custom="${ZSH_CUSTOM:-${ZSH:+$ZSH/custom}}"
if [[ -z "$custom" ]]; then
_omz::log error "\$ZSH is not set. Is Oh My Zsh loaded?"
return 1
fi
local templates="$ZSH/templates/generators/plugin"
if [[ ! -d "$templates" ]]; then
_omz::log error "templates not found at '${templates/#$HOME/\~}'. Try running 'omz update'."
return 1
fi
# Check we can write to $ZSH_CUSTOM/plugins, or the closest directory that exists
local probe="$custom/plugins"
while [[ ! -e "$probe" && "$probe" != "${probe:h}" ]]; do
probe="${probe:h}"
done
if [[ ! -w "$probe" ]]; then
_omz::log error "cannot write to '${probe/#$HOME/\~}'. Set \$ZSH_CUSTOM to a directory you own."
return 1
fi
## Gather answers. Nothing is written until all of them are in.
local name="$1" attempts=0
if [[ -n "$name" ]]; then
_omz::generate::plugin::validate_name "$name" || return 1
elif (( interactive )); then
while true; do
_omz::generate::plugin::ask "Plugin name" || return 1
name="$REPLY"
_omz::generate::plugin::validate_name "$name" && break
if (( ++attempts >= 3 )); then
_omz::log error "giving up."
return 1
fi
done
else
_omz::generate::plugin::usage
return 1
fi
local dir="$custom/plugins/$name"
if [[ -e "$dir" ]]; then
_omz::log error "'$name' already exists at '${dir/#$HOME/\~}'. Remove it or pick another name."
return 1
fi
if [[ -d "$ZSH/plugins/$name" ]]; then
_omz::log warn "'$name' is also a built-in plugin. Your custom plugin will override it."
if (( interactive )); then
_omz::confirm "Continue? [y/N] "
if [[ "$REPLY" != [yY] ]]; then
_omz::log info "aborted."
return 1
fi
fi
fi
local description="${opts[-d]:-${opts[--description]:-}}"
if [[ -z "$description" ]]; then
description="$name plugin for Oh My Zsh"
if (( interactive )); then
_omz::generate::plugin::ask "Short description" "$description" || return 1
description="$REPLY"
fi
fi
# The description goes into a comment and a README line: keep it on one line
description="${description//[[:cntrl:]]/ }"
local cmd="${opts[-c]:-${opts[--command]:-}}"
if [[ -n "$cmd" ]]; then
_omz::generate::plugin::validate_command "$cmd" || return 1
elif (( interactive )); then
local default_cmd=""
(( $+commands[$name] )) && default_cmd="$name"
attempts=0
while true; do
if [[ -n "$default_cmd" ]]; then
_omz::generate::plugin::ask "Command-line tool this plugin wraps (or 'none')" "$default_cmd" || return 1
else
_omz::generate::plugin::ask "Command-line tool this plugin wraps (blank for none)" || return 1
fi
cmd="$REPLY"
[[ "$cmd" != (none|-) ]] || cmd=""
[[ -n "$cmd" ]] || break
_omz::generate::plugin::validate_command "$cmd" && break
if (( ++attempts >= 3 )); then
_omz::log error "giving up."
return 1
fi
done
fi
# completion: "" (none), "file" (a _<cmd> skeleton) or "cached" (the tool generates it)
local completion="" compgen=""
if [[ -n "$cmd" ]] && (( ! ${+opts[--no-completion]} )); then
completion=file
if (( interactive )); then
_omz::confirm "Add a completion function for $cmd? [Y/n] "
if [[ "$REPLY" == [nN] ]]; then
completion=""
else
_omz::confirm "Does $cmd generate its own zsh completion (e.g. '$cmd completion zsh')? [y/N] "
if [[ "$REPLY" == [yY] ]]; then
completion=cached
_omz::generate::plugin::ask "Command that prints the completion script" "$cmd completion zsh" || return 1
compgen="$REPLY"
fi
fi
fi
fi
local enable=${+opts[--enable]}
if (( interactive && ! enable )); then
_omz::confirm "Add $name to plugins=() in your .zshrc now? [y/N] "
[[ "$REPLY" != [yY] ]] || enable=1
fi
## Write the files. If anything fails, remove only what we created.
local -a created
local block="" cache="" ok=0
{
if [[ ! -d "$custom/plugins" ]]; then
command mkdir -p "$custom/plugins" || return 1
_omz::generate::plugin::created "$custom/plugins"
fi
command mkdir "$dir" || return 1
_omz::generate::plugin::created "$dir"
local plugin_tpl=plugin.zsh-template readme_tpl=README.md-template
if [[ -z "$cmd" ]]; then
plugin_tpl=plugin-generic.zsh-template
readme_tpl=README-generic.md-template
elif [[ "$completion" == cached ]]; then
block="$(_omz::generate::plugin::template completion-cached.zsh-template)" || return 1
cache="$(_omz::generate::plugin::template README-cache.md-template)" || return 1
else
block="$(_omz::generate::plugin::template completion-note.zsh-template)" || return 1
fi
_omz::generate::plugin::render "$plugin_tpl" "$dir/$name.plugin.zsh" || return 1
_omz::generate::plugin::render "$readme_tpl" "$dir/README.md" || return 1
if [[ "$completion" == file ]]; then
_omz::generate::plugin::render completion.zsh-template "$dir/_$cmd" || return 1
fi
ok=1
} always {
if (( ! ok )); then
_omz::log error "could not generate the plugin. Cleaning up..."
(( ${#created} )) && command rm -f -- "${created[@]}"
[[ ! -d "$dir" ]] || command rmdir -- "$dir" 2>/dev/null
fi
}
print
_omz::log info "plugin '$name' generated."
print
print -r -- "Next steps:"
print
print -r -- " 1. Edit it: ${EDITOR:-vim} ${dir/#$HOME/\~}/$name.plugin.zsh"
print -r -- " 2. Try it now: omz plugin load $name"
(( enable )) || print -r -- " 3. Keep it: omz plugin enable $name"
print
print -r -- "Docs: https://github.com/ohmyzsh/ohmyzsh/wiki/Customization#overriding-and-adding-plugins"
# Last thing we do: in an interactive shell this restarts zsh
if (( enable )); then
print
if (( ${plugins[(Ie)$name]} )); then
# Already enabled (e.g. a custom override of a built-in): just reload
_omz::log info "'$name' is already in your plugins list."
[[ ! -o interactive ]] || _omz::reload
else
_omz::plugin::enable "$name"
fi
fi
}
function _omz::generate::plugin::usage {
cat >&2 <<EOF
Usage: omz generate plugin [<name>] [options]
Creates a custom plugin in \$ZSH_CUSTOM/plugins/<name>. Anything not given as an
option is asked for interactively. In a non-interactive shell (or with --yes)
defaults are used instead.
Options:
-d, --description <text> One-line description for the README and file header
-c, --command <cmd> Command-line tool this plugin wraps. Adds a check
that it is installed and scaffolds its completion
--no-completion Don't scaffold a completion function
--enable Add the plugin to plugins=() in .zshrc when done
(restarts your shell)
-y, --yes Never prompt; use defaults for anything not given
-h, --help Show this help
EOF
}
function _omz::generate::plugin::complete_options {
local -a opts
opts=(
'--description:One-line description for the README'
'--command:Command-line tool this plugin wraps'
'--no-completion:Skip the completion scaffold'
'--enable:Enable the plugin when done'
'--yes:Never prompt'
)
_describe -o 'options' opts
}
# The name ends up in a mkdir path and, via --enable, inside the awk script that
# rewrites .zshrc. This is the one place it gets checked.
function _omz::generate::plugin::validate_name {
setopt localoptions extendedglob
local name="$1"
if [[ -z "$name" ]]; then
_omz::log error "plugin name cannot be empty."
elif (( ${#name} > 64 )); then
_omz::log error "plugin name is too long (64 characters max)."
elif [[ "$name" != "${name:l}" ]]; then
_omz::log error "plugin names must be lowercase. Did you mean '${name:l}'?"
elif [[ "$name" != [a-z0-9][a-z0-9_-]# ]]; then
_omz::log error "invalid plugin name: use only lowercase letters, digits, '-' and '_', starting with a letter or digit."
else
return 0
fi
return 1
}
# The command name ends up in $+commands[...], _comps[...] and a filename
function _omz::generate::plugin::validate_command {
setopt localoptions extendedglob
if (( ${#1} > 64 )) || [[ "$1" != [[:alnum:]_][[:alnum:]_.+-]# ]]; then
_omz::log error "invalid command name: use only letters, digits, '.', '_', '+' and '-'."
return 1
fi
}
# Ask a question and leave the answer in $REPLY. An empty answer takes the
# default. Returns 1 when there is no more input (Ctrl-D).
function _omz::generate::plugin::ask {
setopt localoptions extendedglob
local question="$1" default="$2"
[[ -z "$default" ]] || question+=" [$default]"
_omz::log prompt "$question: " "omz::generate::plugin"
if ! builtin read -r; then
print >&2
_omz::log info "aborted." "omz::generate::plugin"
return 1
fi
[[ -n "$REPLY" ]] || REPLY="$default"
# Answers are single-line values: flatten control characters and trim
REPLY="${REPLY//[[:cntrl:]]/ }"
REPLY="${${REPLY##[[:space:]]#}%%[[:space:]]#}"
}
# Print a template file with its %placeholders% turned into private markers,
# or explain which one is missing
function _omz::generate::plugin::template {
if [[ ! -f "$templates/$1" ]]; then
_omz::log error "missing template '${templates/#$HOME/\~}/$1'." "omz::generate::plugin"
return 1
fi
local content="$(<"$templates/$1")" token m=$'\x1f'
for token in completion cache name command description compgen; do
content="${content//\%${token}\%/${m}${token}${m}}"
done
print -r -- "$content"
}
# Fill in the placeholders of a template and write it to a file. Values come
# from the caller: $name, $cmd, $description, $compgen, $block and $cache.
#
# Placeholders are matched as the markers that ::template produced, and the
# marker character is stripped from every value, so a value that happens to
# look like a placeholder (e.g. -d '%name%') is written out literally.
function _omz::generate::plugin::render {
setopt localoptions extendedglob
local content m=$'\x1f'
content="$(_omz::generate::plugin::template "$1")" || return 1
# Blocks go first: they are templates too and carry markers of their own
content="${content//${m}completion${m}/$block}"
content="${content//${m}cache${m}/$cache}"
content="${content//${m}name${m}/${name//$m/}}"
content="${content//${m}command${m}/${cmd//$m/}}"
content="${content//${m}description${m}/${description//$m/}}"
content="${content//${m}compgen${m}/${compgen//$m/}}"
# Drop blank lines left behind by an empty placeholder at the end
content="${content%%$'\n'##}"
created+=("$2")
print -r -- "$content" > "$2" || return 1
_omz::generate::plugin::created "$2"
}
function _omz::generate::plugin::created {
# Colour the verb with print -P, but print the path with print -r so that a
# '%' somewhere in the path isn't treated as a prompt escape.
# Keep the output plain when it is being piped.
if [[ -t 1 ]]; then
print -Pn " %F{green}create%f "
else
print -n " create "
fi
print -r -- "${1/#$HOME/\~}"
}
function _omz::plugin {
(( $# > 0 && $+functions[$0::$1] )) || {
cat >&2 <<EOF
@@ -844,5 +1315,3 @@ function _omz::version {
printf "%s (%s)\n" "$version" "$commit"
)
}
omz "$@"
@@ -1,4 +1,4 @@
# omz_diagnostic_dump
# diagnostics.zsh
#
# Diagnostic and debugging support for oh-my-zsh
@@ -351,5 +351,3 @@ function _omz_diag_dump_os_specific_version() {
done
}
omz_diagnostic_dump "$@"
+198
View File
@@ -0,0 +1,198 @@
#!/usr/bin/zsh -df
# Tests for `omz generate plugin`. Run with: zsh lib/tests/generate-plugin.test.zsh
ZSH="${0:A:h:h:h}"
ZSH_CUSTOM="$(mktemp -d)"
trap 'command rm -rf "$ZSH_CUSTOM"' EXIT
source "$ZSH/lib/cli.zsh"
failures=0
pass() { print -u2 "\e[32mSuccess\e[0m: $1" }
fail() { print -u2 "\e[31mError\e[0m: $1"; (( failures++ )) }
# assert_files <plugin> <expected files, space separated>
assert_files() {
local actual="$(print -l "$ZSH_CUSTOM"/plugins/$1/*(N:t) "$ZSH_CUSTOM"/plugins/$1/_*(N:t) | sort -u | tr '\n' ' ')"
local expected="$(print -l ${=2} | sort -u | tr '\n' ' ')"
if [[ "$actual" == "$expected" ]]; then
pass "$1 has files: $expected"
else
fail "$1 has files '$actual', expected '$expected'"
fi
}
# assert_syntax <plugin>: every generated file must pass zsh -n, like CI does
assert_syntax() {
local file
for file in "$ZSH_CUSTOM"/plugins/$1/*.plugin.zsh(N) "$ZSH_CUSTOM"/plugins/$1/_*(N); do
if zsh -n "$file"; then
pass "${file:t} passes zsh -n"
else
fail "${file:t} fails zsh -n"
fi
done
}
# assert_contains <file> <literal string>
assert_contains() {
if grep -qF -- "$2" "$1"; then
pass "${1:t} contains '$2'"
else
fail "${1:t} does not contain '$2'"
fi
}
## Templates must be valid zsh even before substitution (CI checks this too)
for file in "$ZSH"/templates/generators/plugin/*.zsh-template; do
if zsh -n "$file"; then
pass "template ${file:t} passes zsh -n"
else
fail "template ${file:t} fails zsh -n"
fi
done
## A plugin that wraps a command
description='Shortcuts & stuff $(id) `id` 100%'
if omz generate plugin foo -d "$description" -c foo --yes >/dev/null 2>&1; then
pass "generates a command-wrapping plugin"
else
fail "generating a command-wrapping plugin failed"
fi
assert_files foo "README.md _foo foo.plugin.zsh"
assert_syntax foo
assert_contains "$ZSH_CUSTOM/plugins/foo/README.md" 'plugins=(... foo)'
assert_contains "$ZSH_CUSTOM/plugins/foo/README.md" "$description"
assert_contains "$ZSH_CUSTOM/plugins/foo/foo.plugin.zsh" "$description"
assert_contains "$ZSH_CUSTOM/plugins/foo/foo.plugin.zsh" '$+commands[foo]'
assert_contains "$ZSH_CUSTOM/plugins/foo/_foo" '#compdef foo'
if grep -q '%[a-z]*%' "$ZSH_CUSTOM"/plugins/foo/*; then
fail "foo still has unfilled placeholders"
else
pass "foo has no unfilled placeholders"
fi
## Values that look like placeholders are written literally
omz generate plugin lit -d '%compgen% and %name% stay' -c lit --yes >/dev/null 2>&1
assert_contains "$ZSH_CUSTOM/plugins/lit/README.md" '%compgen% and %name% stay'
assert_contains "$ZSH_CUSTOM/plugins/lit/lit.plugin.zsh" '%compgen% and %name% stay'
## A plugin with no command
if omz generate plugin bar --yes >/dev/null 2>&1; then
pass "generates a generic plugin"
else
fail "generating a generic plugin failed"
fi
assert_files bar "README.md bar.plugin.zsh"
assert_syntax bar
assert_contains "$ZSH_CUSTOM/plugins/bar/README.md" 'This plugin does not add any aliases.'
assert_contains "$ZSH_CUSTOM/plugins/bar/bar.plugin.zsh" 'bar plugin for Oh My Zsh'
## --no-completion, and the rails-style output
output="$(omz generate plugin baz -c baz --no-completion --yes 2>/dev/null)"
assert_files baz "README.md baz.plugin.zsh"
create_lines=$(print -r -- "$output" | grep -c '^ create ')
if (( create_lines == 3 )); then
pass "prints one create line per path"
else
fail "expected 3 create lines, got $create_lines"
fi
if [[ "$output" == *"omz plugin load baz"* ]]; then
pass "prints next steps"
else
fail "next steps missing from output"
fi
## Bad names are rejected without touching the filesystem
before="$(print -l "$ZSH_CUSTOM"/plugins/*(N:t))"
for bad in '' 'Foo' 'a b' '../evil' 'a/b' '.hidden' '.' '..' '-x' 'foo`id`' 'foo"$(id)"' $'a\nb' "$(printf 'x%.0s' {1..65})"; do
if omz generate plugin "$bad" --yes >/dev/null 2>&1; then
fail "accepted bad name ${(qq)bad}"
else
pass "rejected bad name ${(qq)bad}"
fi
done
after="$(print -l "$ZSH_CUSTOM"/plugins/*(N:t))"
if [[ "$before" == "$after" ]]; then
pass "bad names created nothing"
else
fail "bad names changed \$ZSH_CUSTOM/plugins"
fi
## Bad command names are rejected
for bad in 'a b' '../x' 'foo;id' 'foo$(id)'; do
if omz generate plugin qux -c "$bad" --yes >/dev/null 2>&1; then
fail "accepted bad command ${(qq)bad}"
else
pass "rejected bad command ${(qq)bad}"
fi
done
## Existing plugin, unknown option, missing templates
if omz generate plugin foo --yes >/dev/null 2>&1; then
fail "overwrote an existing plugin"
else
pass "refuses to overwrite an existing plugin"
fi
if omz generate plugin quux --bogus --yes >/dev/null 2>&1; then
fail "accepted an unknown option"
else
pass "rejects an unknown option"
fi
if ( ZSH=/nonexistent; omz generate plugin quux --yes >/dev/null 2>&1 ); then
fail "ran without templates"
else
pass "fails cleanly when templates are missing"
fi
[[ -e "$ZSH_CUSTOM/plugins/quux" ]] && fail "quux was created by a failed run"
## A failure after the first file is written removes everything it created
broken="$(mktemp -d)"
mkdir -p "$broken/templates/generators/plugin" "$broken/plugins"
cp "$ZSH/templates/generators/plugin/plugin.zsh-template" \
"$ZSH/templates/generators/plugin/completion-note.zsh-template" "$broken/templates/generators/plugin/"
# README.md-template is missing, so the plugin file is written and then the README fails
if ( ZSH="$broken"; omz generate plugin partial -c partial --yes >/dev/null 2>&1 ); then
fail "succeeded with a missing README template"
else
pass "fails when a later template is missing"
fi
if [[ -e "$ZSH_CUSTOM/plugins/partial" ]]; then
fail "partial plugin directory was left behind: $(ls "$ZSH_CUSTOM/plugins/partial")"
else
pass "cleans up the partial plugin directory"
fi
command rm -rf "$broken"
## The prompt helper
_omz::generate::plugin::ask "Q" "dflt" 2>/dev/null < <(print '')
[[ "$REPLY" == "dflt" ]] && pass "ask: empty answer takes the default" || fail "ask: got '$REPLY', expected 'dflt'"
_omz::generate::plugin::ask "Q" "dflt" 2>/dev/null < <(print ' hi there ')
[[ "$REPLY" == "hi there" ]] && pass "ask: trims whitespace" || fail "ask: got '$REPLY', expected 'hi there'"
if _omz::generate::plugin::ask "Q" 2>/dev/null < /dev/null; then
fail "ask: did not fail on EOF"
else
pass "ask: fails on EOF"
fi
print -u2
if (( failures )); then
print -u2 "\e[31m$failures test(s) failed\e[0m"
exit 1
fi
print -u2 "\e[32mAll tests passed\e[0m"
-3
View File
@@ -78,9 +78,6 @@ fpath=($ZSH/{functions,completions} $ZSH_CUSTOM/{functions,completions} $fpath)
# Load all stock functions (from $fpath files) called below.
autoload -U compaudit compinit zrecompile
# The omz CLI and omz_diagnostic_dump are loaded on first use
autoload -Uz omz omz_diagnostic_dump
is_plugin() {
local base_dir=$1
local name=$2
-19
View File
@@ -19,54 +19,35 @@ plugins=(... jj)
| jjbd | `jj bookmark delete` |
| jjbf | `jj bookmark forget` |
| jjbl | `jj bookmark list` |
| jjblt | `jj bookmark list --tracked` |
| jjbm | `jj bookmark move` |
| jjbr | `jj bookmark rename` |
| jjbs | `jj bookmark set` |
| jjbt | `jj bookmark track` |
| jjbu | `jj bookmark untrack` |
| jjc | `jj commit` |
| jjcfg | `jj config` |
| jjcfgl | `jj config list` |
| jjcmsg | `jj commit --message` |
| jjd | `jj diff` |
| jjdmsg | `jj desc --message` |
| jjds | `jj desc` |
| jjdst | `jj diff --stat` |
| jje | `jj edit` |
| jjev | `jj evolog` |
| jjf | `jj file` |
| jjfl | `jj file list` |
| jjg | `jj git` |
| jjgcl | `jj git clone` |
| jjgf | `jj git fetch` |
| jjgfa | `jj git fetch --all-remotes` |
| jjgi | `jj git init` |
| jjgp | `jj git push` |
| jjgpa | `jj git push --all` |
| jjgpd | `jj git push --deleted` |
| jjgpt | `jj git push --tracked` |
| jjgrl | `jj git remote list` |
| jjl | `jj log` |
| jjla | `jj log -r "all()"` |
| jjn | `jj new` |
| jjnt | `jj new "trunk()"` |
| jjop | `jj op` |
| jjopl | `jj op log` |
| jjor | `jj op restore` |
| jjrb | `jj rebase` |
| jjrbm | `jj rebase -d "trunk()"` |
| jjrs | `jj restore` |
| jjrt | `cd "$(jj root \|\| echo .)"` |
| jjs | `jj show` |
| jjsp | `jj split` |
| jjsq | `jj squash` |
| jjst | `jj status` |
| jju | `jj undo` |
| jjw | `jj workspace` |
| jjwa | `jj workspace add` |
| jjwf | `jj workspace forget` |
| jjwl | `jj workspace list` |
## Prompt usage
-17
View File
@@ -45,51 +45,34 @@ alias jjbc='jj bookmark create'
alias jjbd='jj bookmark delete'
alias jjbf='jj bookmark forget'
alias jjbl='jj bookmark list'
alias jjblt='jj bookmark list --tracked'
alias jjbm='jj bookmark move'
alias jjbr='jj bookmark rename'
alias jjbs='jj bookmark set'
alias jjbt='jj bookmark track'
alias jjbu='jj bookmark untrack'
alias jjc='jj commit'
alias jjcfg='jj config'
alias jjcfgl='jj config list'
alias jjcmsg='jj commit --message'
alias jjd='jj diff'
alias jjdmsg='jj desc --message'
alias jjds='jj desc'
alias jjdst='jj diff --stat'
alias jje='jj edit'
alias jjev='jj evolog'
alias jjf='jj file'
alias jjfl='jj file list'
alias jjg='jj git'
alias jjgcl='jj git clone'
alias jjgf='jj git fetch'
alias jjgfa='jj git fetch --all-remotes'
alias jjgi='jj git init'
alias jjgp='jj git push'
alias jjgpa='jj git push --all'
alias jjgpd='jj git push --deleted'
alias jjgpt='jj git push --tracked'
alias jjgrl='jj git remote list'
alias jjl='jj log'
alias jjla='jj log -r "all()"'
alias jjn='jj new'
alias jjnt='jj new "trunk()"'
alias jjop='jj op'
alias jjopl='jj op log'
alias jjor='jj op restore'
alias jjrb='jj rebase'
alias jjrbm='jj rebase -d "trunk()"'
alias jjrs='jj restore'
alias jjrt='cd "$(jj root || echo .)"'
alias jjs='jj show'
alias jjsp='jj split'
alias jjsq='jj squash'
alias jjst='jj status'
alias jju='jj undo'
alias jjw='jj workspace'
alias jjwa='jj workspace add'
alias jjwf='jj workspace forget'
alias jjwl='jj workspace list'
-1
View File
@@ -71,7 +71,6 @@ plugins=(... kubectl)
| | | **Secret management** |
| kgsec | `kubectl get secret` | Get secret for decoding |
| kgseca | `kubectl get secret --all-namespaces` | List secrets across all namespaces |
| kesec | `kubectl edit secret` | Edit secret resource |
| kdsec | `kubectl describe secret` | Describe secret resource in detail |
| kdelsec | `kubectl delete secret` | Delete the secret |
| | | **Deployment management** |
-1
View File
@@ -93,7 +93,6 @@ alias kdelcm='kubectl delete configmap'
# Secret management
alias kgsec='kubectl get secret'
alias kgseca='kubectl get secret --all-namespaces'
alias kesec='kubectl edit secret'
alias kdsec='kubectl describe secret'
alias kdelsec='kubectl delete secret'
+13 -21
View File
@@ -15,8 +15,8 @@
# - https://github.com/symfony/symfony/blob/5.4/src/Symfony/Component/Console/Resources/completion.bash
#
_sf_console() {
local lastParam out comp sf_cmd
local -a completions flagPrefix requestComp inputs
local lastParam flagPrefix requestComp out comp
local -a completions
# The user could have moved the cursor backwards on the command-line.
# We need to trigger completion from the $CURRENT location, so we need
@@ -29,20 +29,11 @@ _sf_console() {
setopt local_options BASH_REMATCH
if [[ "${lastParam}" =~ '-.*=' ]]; then
# We are dealing with a flag with an =
flagPrefix=(-P "${BASH_REMATCH}")
flagPrefix="-P ${BASH_REMATCH}"
fi
# Prepare the command to obtain completions. An alias is resolved here,
# because the request is no longer read again by the shell.
sf_cmd="${words[1]}"
if [[ -n "${aliases[$sf_cmd]}" ]]; then
requestComp=(${(z)aliases[$sf_cmd]})
else
requestComp=(${~sf_cmd})
fi
requestComp+=(_complete --no-interaction -szsh -a1 "-c$((CURRENT-1))")
# Prepare the command to obtain completions
requestComp="${words[0]} ${words[1]} _complete --no-interaction -szsh -a1 -c$((CURRENT-1))" i=""
for w in ${words[@]}; do
w=$(printf -- '%b' "$w")
# remove quotes from typed values
@@ -56,18 +47,19 @@ _sf_console() {
fi
# empty values are ignored
if [ ! -z "$w" ]; then
inputs+=("-i$w")
i="${i}-i${w} "
fi
done
# Ensure at least 1 input
if (( ! $#inputs )); then
inputs=(-i' ')
if [ "${i}" = "" ]; then
requestComp="${requestComp} -i\" \""
else
requestComp="${requestComp} ${i}"
fi
# The request is run without being read again by the shell, so that a
# "$(...)" or a backtick typed on the command line is not executed
out=$(SHELL_VERBOSITY=0 "${requestComp[@]}" "${inputs[@]}" 2>/dev/null)
# Use eval to handle any environment variables and such
out=$(eval ${requestComp} 2>/dev/null)
while IFS='\n' read -r comp; do
if [ -n "$comp" ]; then
@@ -83,7 +75,7 @@ _sf_console() {
done < <(printf "%s\n" "${out[@]}")
# Let inbuilt _describe handle completions
_describe "completions" completions "${flagPrefix[@]}"
eval _describe "completions" completions $flagPrefix
return $?
}
@@ -0,0 +1,7 @@
## Cache
This plugin caches the completion script and automatically updates it when the
plugin is loaded, which is usually when you start a new terminal emulator.
The cache is stored at `$ZSH_CACHE_DIR/completions/_%command%`.
@@ -0,0 +1,11 @@
# %name% plugin
%description%
To use it, add `%name%` to the plugins array in your zshrc file:
```zsh
plugins=(... %name%)
```
This plugin does not add any aliases.
@@ -0,0 +1,20 @@
# %name% plugin
%description%
To use it, add `%name%` to the plugins array in your zshrc file:
```zsh
plugins=(... %name%)
```
## Aliases
| Alias | Command | Description |
| :----------- | ------------------ | :-------------------- |
| `%command%s` | `%command% status` | Show %command% status |
## Requirements
This plugin requires [%command%](https://example.com) to be installed.
%cache%
@@ -0,0 +1,22 @@
#
# Completion
#
# %command% generates its own zsh completion, so we cache it instead of
# maintaining a `_%command%` file by hand.
#
# On the first shell after installing this plugin the cache file doesn't exist
# yet, so compinit hasn't bound it. Do that manually here.
if [[ ! -f "$ZSH_CACHE_DIR/completions/_%command%" ]]; then
typeset -g -A _comps
autoload -Uz _%command%
_comps[%command%]=_%command%
fi
# Regenerate the cache in the background so it never blocks shell startup.
# TMPPREFIX puts the temp file next to the destination, which keeps the move
# atomic.
zmodload -F zsh/files b:zf_mv
() {
local TMPPREFIX="$ZSH_CACHE_DIR/completions/_%command%"
zf_mv -f -- =( %compgen% ) "$TMPPREFIX"
} &|
@@ -0,0 +1,6 @@
#
# Completion
#
# Oh My Zsh adds this directory to $fpath, so a file named `_%command%` next to
# this one is picked up automatically as the completion for %command%.
#
@@ -0,0 +1,32 @@
#compdef %command%
#
# Completion for %command%.
#
# This is a starting point. Delete what you don't need. Two references:
# https://zsh.sourceforge.io/Doc/Release/Completion-System.html
# https://github.com/zsh-users/zsh/blob/master/Etc/completion-style-guide
local -a subcommands
subcommands=(
'status:Show the current status'
'run:Run something'
'help:Show help for a command'
)
_arguments -C \
'(-h --help)'{-h,--help}'[show help]' \
'(-v --version)'{-v,--version}'[show the version]' \
'1: :->subcommand' \
'*:: :->args'
case $state in
subcommand)
_describe -t commands '%command% subcommand' subcommands
;;
args)
case $words[1] in
status) _arguments '--short[one-line output]' ;;
run) _files ;;
esac
;;
esac
@@ -0,0 +1,48 @@
# %name% plugin
#
# %description%
#
# This file is sourced by Oh My Zsh in every new shell, so keep it fast.
# Avoid running external commands at the top level unless you really need to.
#
# Wrapping a command-line tool? Guard everything so people who don't have it
# installed don't end up with broken aliases:
#
# if (( ! $+commands[sometool] )); then
# return
# fi
#
# Aliases
#
# Document every alias in README.md. Short, obvious, and few beats many.
#
# alias %name%='echo "hello from %name%"'
#
# Functions
#
# Anything that needs arguments in the middle, or more than one command,
# belongs here rather than in an alias.
#
# function %name%_hello() {
# echo "hello, ${1:-world}"
# }
#
# Completion
#
# Oh My Zsh adds this directory to $fpath, so a file named `_sometool` next to
# this one is picked up automatically as the completion for `sometool`.
#
#
# Need the path to this plugin's own directory (for data files, a lib/ dir...)?
# This is the standard way to get it, since $0 is not reliable on its own:
# https://zdharma-continuum.github.io/Zsh-100-Commits-Club/Zsh-Plugin-Standard.html
#
# 0="${ZERO:-${${0:#$ZSH_ARGZERO}:-${(%):-%N}}}"
# 0="${${(M)0:#/*}:-$PWD/$0}"
# source "${0:A:h}/lib/helpers.zsh"
@@ -0,0 +1,43 @@
# %name% plugin
#
# %description%
#
# This file is sourced by Oh My Zsh in every new shell, so keep it fast.
# Avoid running external commands at the top level unless you really need to.
# Only define anything if the tool is actually installed. Without this guard,
# people who don't have %command% get aliases that fail with "command not found".
if (( ! $+commands[%command%] )); then
return
fi
#
# Aliases
#
# Document every alias in README.md. Short, obvious, and few beats many.
# The one below is just an example: replace it with something %command% can do.
#
alias %command%s='%command% status'
#
# Functions
#
# Anything that needs arguments in the middle, or more than one command,
# belongs here rather than in an alias.
#
# function %name%_current() {
# %command% status --short "$@"
# }
%completion%
#
# Need the path to this plugin's own directory (for data files, a lib/ dir...)?
# This is the standard way to get it, since $0 is not reliable on its own:
# https://zdharma-continuum.github.io/Zsh-100-Commits-Club/Zsh-Plugin-Standard.html
#
# 0="${ZERO:-${${0:#$ZSH_ARGZERO}:-${(%):-%N}}}"
# 0="${${(M)0:#/*}:-$PWD/$0}"
# source "${0:A:h}/lib/helpers.zsh"