Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .github/instructions/zsh-scripting.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,9 +236,12 @@ function:
```zsh
() {
builtin emulate -L zsh
setopt local_options

local -r source_path=${1:a}
local -r plugin_dir=${source_path:h}

# Reusable work.
}
} "${ZERO:-${${0:#$ZSH_ARGZERO}:-${(%):-%N}}}"
```

## Autoload functions and completions
Expand Down
4 changes: 2 additions & 2 deletions .github/skills/new-zsh-plugin/templates/plugin.plugin.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
() {
builtin emulate -L zsh

typeset -r source_path="${${(M)1:#/*}:-$PWD/$1}"
typeset -r plugin_dir=${source_path:a:h}
local -r source_path=${1:a}
local -r plugin_dir=${source_path:h}

# Source private eager helpers from "$plugin_dir/lib" only when required.
# Keep setup-only functions local to this loader. Autoloaded functions and
Expand Down
99 changes: 90 additions & 9 deletions scripts/test_validate_zsh_standard_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -1990,7 +1990,7 @@ def test_repair_2_consumer_parser_outputs_match_frozen_golden(self) -> None:

self.assertEqual(
digest,
"e385f4d4c34496495e808b602b59adb88d5ace192619e04669c70f4020afc981",
"1a958a39d678afef3c4a42fa8163a131d8799e7adbc457403147c31eeff0ec19",
)

def test_rejects_list_and_nested_container_rule_headings(self) -> None:
Expand Down Expand Up @@ -3472,6 +3472,20 @@ def test_rendered_plugin_template_restores_lifecycle_state(self) -> None:
"__IDENTIFIER__",
"demo",
)
rendered = rendered.replace(
' # Source private eager helpers from "$plugin_dir/lib" only when required.',
textwrap.indent(
textwrap.dedent(r"""
[[ $source_path == ${DEMO_EXPECTED_SOURCE_PATH-} ]] || return 90
[[ $plugin_dir == ${DEMO_EXPECTED_PLUGIN_DIR-} ]] || return 91
[[ ${(t)source_path} == *readonly* ]] || return 92
[[ ${(t)plugin_dir} == *readonly* ]] || return 93

# Source private eager helpers from "$plugin_dir/lib" only when required.
""").strip(),
" ",
),
)
entry_path.write_text(rendered, encoding="utf-8")

environment = os.environ.copy()
Expand All @@ -3490,6 +3504,8 @@ def test_rendered_plugin_template_restores_lifecycle_state(self) -> None:
typeset caller_zero=$0
typeset -ga fpath=( /baseline )
typeset -gA Plugins=( OTHER caller-other )
typeset -g DEMO_EXPECTED_SOURCE_PATH=$2
typeset -g DEMO_EXPECTED_PLUGIN_DIR=$3

. "$1" || exit 10
(( ${+functions[demo_plugin_unload]} )) || exit 11
Expand All @@ -3508,15 +3524,70 @@ def test_rendered_plugin_template_restores_lifecycle_state(self) -> None:
[[ ${Plugins[OTHER]} == caller-other ]] || exit 33
[[ $0 == "$caller_zero" ]] || exit 34
""")
direct_source = str(entry_path)
direct_relative_source = str(
entry_path.relative_to(temporary_directory)
)
manager_source = str(
plugin_root / "manager [literal]*? plugin.plugin.zsh"
)
manager_raw_source = str(
plugin_root
/ "discarded [literal]*? segment"
/ ".."
/ "manager [literal]*? plugin.plugin.zsh"
)
cases = {
"default-native": "",
"caller-no-function-argzero": "unsetopt function_argzero",
"caller-posix-argzero": "setopt posix_argzero",
"caller-ksh-arrays": "setopt ksh_arrays",
"caller-no-unset": "setopt no_unset",
"caller-hostile-globbing": "setopt glob_subst glob_assign",
"default-native": ("", direct_source, direct_source, direct_source),
"direct-relative-special-path": (
"",
direct_relative_source,
direct_source,
direct_source,
),
"manager-zero-normalized": (
"ZERO=$4",
direct_source,
manager_source,
manager_raw_source,
),
"caller-no-function-argzero": (
"unsetopt function_argzero",
direct_source,
direct_source,
direct_source,
),
"caller-posix-argzero": (
"setopt posix_argzero",
direct_source,
direct_source,
direct_source,
),
"caller-ksh-arrays": (
"setopt ksh_arrays",
direct_source,
direct_source,
direct_source,
),
"caller-no-unset": (
"setopt no_unset",
direct_source,
direct_source,
direct_source,
),
"caller-hostile-globbing": (
"setopt glob_subst glob_assign",
direct_source,
direct_source,
direct_source,
),
}
for name, setup in cases.items():
for name, (
setup,
source,
expected_source,
manager_source_arg,
) in cases.items():
with self.subTest(case=name):
completed = subprocess.run( # nosec B603
[
Expand All @@ -3525,12 +3596,16 @@ def test_rendered_plugin_template_restores_lifecycle_state(self) -> None:
"-c",
setup + "\n" + lifecycle,
"zsh",
str(entry_path),
source,
expected_source,
str(Path(expected_source).parent),
manager_source_arg,
],
check=False,
capture_output=True,
text=True,
env=environment,
cwd=temporary_directory,
timeout=10,
)
self.assertEqual(
Expand Down Expand Up @@ -3637,6 +3712,8 @@ def test_public_zsh_consumers_defer_to_canonical_standard(self) -> None:
self.assertNotIn('\n0="', template)
self.assertEqual(template.count("builtin emulate -L zsh"), 2)
for fragment in (
"local -r source_path=${1:a}",
"local -r plugin_dir=${source_path:h}",
"__IDENTIFIER___plugin_unload",
"unfunction __IDENTIFIER___plugin_unload",
":__IDENTIFIER__:config",
Expand All @@ -3646,6 +3723,7 @@ def test_public_zsh_consumers_defer_to_canonical_standard(self) -> None:
for fragment in ("${PMSPEC-}", "Plugins[", "__KEY__"):
with self.subTest(portable_template_excludes=fragment):
self.assertNotIn(fragment, template)
self.assertNotIn("${${(M)1:#/*}:-$PWD/$1}", template)

readme = (PUBLIC_ROOT / ".github/README.md").read_text(encoding="utf-8")
self.assertIn("scripts/validate-zsh-standard-policy.py", readme)
Expand Down Expand Up @@ -3724,6 +3802,9 @@ def test_lifecycle_harness_is_option_sensitive_bounded_and_zero_neutral(
('"caller-posix-argzero"', 1),
('"caller-no-function-argzero"', 1),
('"caller-hostile-globbing"', 1),
('"direct-relative-special-path"', 1),
('"manager-zero-normalized"', 1),
("[[ ${(t)source_path} == *readonly* ]]", 1),
("[[ ${Plugins[OTHER]} == caller-other ]]", 2),
)
for fragment, minimum_count in requirements:
Expand Down
Loading