name: zsh-writer description: Use when writing or modifying ZSH functions.
ZSH Writer
Overview
Write ZSH code that is consistent with my conventions.
Core Workflow
Step 1 — Place the file
Goal: Correct path and name.
Exit criterion: File exists at correct path with correct name.
- Autoloaded (preferred, called from zsh):
tools/term/zsh/config/functions/autoload/{domain}/{subdomain?}/{name} - Standalone (called externally like from keybindings):
scripts/bin/{domain}/{subdomain?}/{name} - Name:
{domain}-{subdomain?}-{action}— e.g.git-branch-list,json-lint -rawsuffix → machine-readable▮-separated output, consumed by other scripts
Step 2 — TDD: Write a failing test
Goal: Ensure the bug/feature has a failing test first
Exit criterion: Test fails.
Write a failing test for the bug or missing feature you want to implement.
- Run
bats <test_filepath>to run the tests - See Testing for full examples and best practices
bats_load_library 'helper'
setup() {
bats_tmp_dir
}
teardown() {
bats_cleanup
}
@test "converts space-separated words to camelCase" {
bats_run_zsh "my-new-function hello world"
[ "$status" -eq 0 ]
[ "$output" = "helloWorld" ]
}
@test "passes result to clipboard" {
pbcopy() { echo "$1" > "$BATS_TMP_DIR/clipboard.txt"; }
bats_mock pbcopy
bats_run_zsh "my-new-function hello world"
[ "$(cat "$BATS_TMP_DIR/clipboard.txt")" = "helloWorld" ]
}
Step 3 — Write the code
Goal: Working code, following coding style.
Exit criterion: Test passes.
Write code that follows the following patterns:
| Pattern | Rule |
|---|---|
| Headers | Top of the file: what the script does, how to call it and error protection |
| Args parsing | Use zparseopts to parse --named arguments |
| Variables | local myVar="$(myCommand)" on one line |
| Splitting | Use ▮ as separator and ${(@ps/▮/)line} to split |
| Conditions | [[ simpleCondition ]] && state=value. No nested if/else, return early |
| Calling Commands | Use existing helpers (git-branch-current), not raw calls. Use --long-form, not -l. |
# Show changed files with syntax-aware coloring
# Usage:
# $ git-diff-colorize # Unstaged changes
# $ git-diff-colorize --staged # Staged only
# $ git-diff-colorize --ext ts # Filter by extension
setopt local_options err_return
MAX_RESULTS=50
zmodload zsh/zutil
zparseopts -E -D \
s=flagStaged \
-staged=flagStaged \
e:=flagExt \
-ext:=flagExt
local isStaged=${#flagStaged}
local extFilter=${flagExt[2]}
local helperArgs=()
[[ $isStaged == 1 ]] && helperArgs+=(--staged)
[[ "$extFilter" != "" ]] && helperArgs+=(--ext "$extFilter")
local rawFiles="$(git-diff-list-raw "${helperArgs[@]}")"
# Nothing to display if working tree is clean
[[ "$rawFiles" == "" ]] && return 0
local output=""
for rawLine in ${(f)rawFiles}; do
local fields=(${(@ps/▮/)rawLine})
local name=$fields[1]
local dir=$fields[2]
local ext=$fields[3]
local parentDir="${dir:t}" # zsh modifier: last path component
local color=$COLOR_DEFAULT
[[ "$ext" == "ts" ]] && color=$COLOR_FILE_TS
[[ "$ext" == "zsh" ]] && color=$COLOR_FILE_ZSH
output+="$(colorize "$name" $color)▮$parentDir▮$ext\n"
done
table $output
Step 4 — Lint the file
Run zsh-lint <file> on any modified .zsh files.
Run bats-lint <test_file> on any modified .bats test files.
Fix every violation, (including pre-existing ones not introduced by the current change).
Common Rationalizations
| Rationalization | Reality |
|---|---|
| "It's only two levels of if/else, it's ok." | No it's not. Return early, always. |
Checklist
- Quick documentation and usage at top of script
- Return early — no avoidable nesting
- Comments for each guard clause
- All function vars
local; script constants UPPER_CASE withoutlocal - External commands use long-form args, one per line
- Use existing helpers over porcelain (e.g.
git-branch-list-rawnotgit branch) - Use
zparseoptsfor --named arguments - Tests use the dedicated helpers