mirror of
https://github.com/robbyrussell/oh-my-zsh.git
synced 2026-09-26 21:16:05 +02:00
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>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
593e8ed6c8
commit
46aee25f27
@@ -0,0 +1,173 @@
|
||||
#!/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
|
||||
|
||||
## 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"
|
||||
|
||||
## 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"
|
||||
Reference in New Issue
Block a user