Skip to main content

zsh-gotchas

Four zsh expansions that silently rewrite a command: $VAR:word modifiers, a leading =, no word splitting, `path` tied to PATH. Use when a zsh one-liner gives a wrong result or a tool seems missing.

설치로 이동

소스 정보

저장소
laurigates/claude-plugins
최근 소스 활동
2026년 9월 21일 11:45
감지된 SKILL.md 언어
영어
스타
58
포크
6

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
zsh-gotchas
description
Four zsh expansions that silently rewrite a command: $VAR:word modifiers, a leading =, no word splitting, `path` tied to PATH. Use when a zsh one-liner gives a wrong result or a tool seems missing.
allowed-tools
Bash, Read
created
2026-09-21T00:00:00.000Z
modified
2026-09-21T00:00:00.000Z
reviewed
2026-09-21T00:00:00.000Z
# Zsh Gotchas — Four Expansions That Rewrite Your Command Four zsh behaviours with no bash equivalent. Each one fires on **ordinary commands**, including the one-liners an agent runs through the Bash tool — not just on `.zsh` files — so a snippet lifted from a bash-tested doc breaks only here. What makes them expensive is that three of the four **fail silently or misattribute**: the command runs, exits, and reports something that looks like a real answer about the world. Every one below was diagnosed the wrong way first. | Symptom | Mechanism | Section | |---|---|---| | A URL/path with `$var:` hits the wrong endpoint; fast empty 404 | `$VAR:x` is a history modifier | [1](#1-varword-is-a-modifier-not-a-colon-after-a-variable) | | A later command in a `;` chain produced no output | a word starting with `=` is a command lookup | [2](#2-a-word-starting-with--is-a-command-lookup) | | `$2` empty after `set -- $spec`; tool prints its usage text | zsh does not word-split | [3](#3-zsh-does-not-word-split-an-unquoted-parameter) | | `command not found` for a tool that is installed | `path` is tied to `PATH` | [4](#4-path-is-not-a-free-variable-name) | Out of scope: zsh-vs-POSIX pattern expansion and extended glob. Those bite when writing `.zsh` / `zshrc` files, not on every command, and are not covered here. ## When to Use This Skill | Use this skill when... | Use `shell-expert` instead when... | |---|---| | A zsh command ran and returned a result that does not match what you typed | Writing a shell script, function, or pipeline from scratch | | An unexpected 404, an empty positional, or a missing chunk of output from a chain | Choosing portable constructs across bash / zsh / POSIX | | `command not found` names a tool you know is installed | Structuring error handling, `set` flags, or CLI argument parsing | | A bash-tested snippet behaves differently once run through the Bash tool | The question is shell *style* rather than a wrong runtime result | The split is runtime versus authoring: this skill covers four expansions that make a correct-looking zsh command do something else, three of them silently. ## 1. `$VAR:word` Is a Modifier, Not a Colon After a Variable Zsh applies history-style **modifiers** directly to a bare parameter expansion: `$f:h` (dirname), `$f:t` (basename), `$f:r`, `$f:e`, `$f:s/a/b/`, `$f:g…`. So a URL that puts a variable straight before a colon is not the URL you wrote: ```zsh # Wrong — `:g` is read as the start of a global-substitution modifier curl "https://…/v1beta/models/$MODEL:generateContent" ``` Observed 2026-09-05 (robocar-unified, probing a replacement Gemini model): three requests came back `HTTP 404` in **60 ms with an empty body**, and were nearly reported as "the new model does not support generateContent". The empty body and the sub-100 ms turnaround were the tell — a real API 404 carries a JSON error and takes a round trip. The same request with braces returned 200. ### The fix Brace the expansion. Braces end the parameter name, so the colon is literal: ```zsh curl "https://…/v1beta/models/${MODEL}:generateContent" ``` Double quotes do **not** protect you — modifiers apply inside them. ### When it bites - Gemini-style `model:method` REST paths, `host:port`, `user:group`, `file:line`, `scp`/`rsync` `host:path` targets — any `$var:` with a letter after the colon. - A modifier letter that zsh does not recognise errors loudly (`unrecognized modifier`); the recognised ones (`g`, `h`, `t`, `r`, `e`, `s`, `a`, `A`, `l`, `u`, `q`, `Q`, `x`, `c`, `P`) fail silently by rewriting the string. ## 2. A Word Starting With `=` Is a Command Lookup With `EQUALS` set (the zsh default), an unquoted word that begins with `=` is replaced by the path of the command named after it: `=ls` becomes `/bin/ls`. When no such command exists, the expansion is a **fatal error that aborts the rest of the command line**, not just that word, and `;` does not contain it: ```zsh zsh -fc 'echo a; echo ======; echo b' # a # zsh:1: ===== not found ← `echo b` never runs, exit 1 ``` Observed 2026-09-15 (claude-plugins, session-end): `survey.sh …; echo ======; distill-survey.sh …` printed the survey, then `(eval):1: ===== not found`, and the second collector silently never ran. The error names the word minus its leading `=`, which reads like a missing command rather than a separator. ### The fix Quote the word (`echo '======'`), or use a separator that does not start with `=` (`echo ---`). `setopt noequals` also works but changes the shell for everything after it. ### When it bites - Section separators in chained diagnostics (`echo =====`, `print ==== x`). - Arguments that start with `=`: `--flag =value`, `git log =main`, a jq or awk program passed unquoted. - Mid-chain placement: everything after the bad word is lost, so the symptom is missing output from a later command, not an error at the one you wrote. ## 3. Zsh Does Not Word-Split an Unquoted Parameter Bash splits an unquoted `$var` on `IFS`; zsh does not, because `SH_WORD_SPLIT` is off by default. The whole string stays one word, so `$1` is everything and `$2` is empty: ```zsh # Wrong — $2 is empty, so gh runs without a PR number for spec in "ForumViriumHelsinki/infrastructure 2379" "ForumViriumHelsinki/.github 127"; do set -- $spec gh pr view "$2" -R "$1" --json state done ``` Observed 2026-09-16 (verifying three PRs after a merge): the loop printed gh's usage text three times — *"argument required when using the --repo flag"* — and verified nothing. The message names a **flag**, so it reads as a wrong invocation rather than an empty variable, and the obvious next move is to rewrite the `gh` call that was already correct. ### The fix Force splitting with `${=var}`, or read into named variables: ```zsh for spec in "ForumViriumHelsinki/infrastructure 2379"; do read -r repo num <<< "$spec"; gh pr view "$num" -R "$repo" --json state; done ``` Named variables are the better habit: they survive a copy into a bash script, and they say what each field is. ### When it bites - `set -- $line` / `set -- $spec` over a list of space-separated records — the natural way to unpack "repo number" or "host port" pairs. - `cmd $args` where `args` holds several flags. Zsh passes them as **one** argument; the tool reports an unknown option containing spaces. - Splitting on something other than whitespace: `${(s:,:)csv}` in zsh, not `IFS=, read`. ### The failure looks like a clean result, not an error Both halves of that session's evidence were silent. The `gh` loop printed usage text, and a malformed taskwarrior query minutes later (`task export status:pending` — taskwarrior wants the filter **before** the command) returned three empty lists that read exactly like an empty queue. A control re-run with the correct order returned 457 tasks. Control-test any negative that gates an action. ## 4. `path` Is Not a Free Variable Name Zsh **ties `path` to `PATH`**: `path` is the array view, `PATH` the scalar view, and writing either rewrites the other. Assigning a string to `path` therefore destroys the search path for the rest of the shell. The usual way in is a loop variable chosen for readability: ```zsh # Wrong — `path` is special; PATH is destroyed on the FIRST iteration while IFS=$'\t' read -r key title labels path; do gh issue create --title "$title" --body-file "$path" ... done < list.tsv ``` Observed 2026-09-02 (silverbucket-helper, filing eight issues): every iteration printed `command not found: tail`, and **nothing was created** — `gh` was already unreachable by the time the body ran. Zsh's tied variables are a small set, and the rest are just as ordinary looking: `path`, `cdpath`, `fpath`, `manpath`, `fignore`, `mailpath`, `module_path`, `prompt`, `psvar`, `status`, `argv`. `path` and `fpath` are the two a script is most likely to reach for by accident. ### The fix Pick a name that is not tied. Any of `bodyfile`, `file`, `p`, `target` works; the rename is the whole fix. ```zsh while IFS=$'\t' read -r key title labels bodyfile; do gh issue create --title "$title" --body-file "$bodyfile"; done < list.tsv ``` `typeset` does not rescue you — `local path` inside a function still shadows the tied parameter and still breaks command lookup for that function's body. ### Check whether anything actually ran The failure is loud but **misattributed**: zsh reports the missing command, not the cause, so the obvious next move is to fix the "missing" tool. Before retrying a loop that had side effects, establish whether the side effects happened — a partially-completed run retried from the top creates duplicates. ```zsh gh issue list --state open --limit 30 --json number,createdAt ``` In the case above the answer was none, because `PATH` died on iteration one before `gh` was reached. Had it died later, some issues would exist and a blind retry would have double-filed them. ## Verifying a Suspected Hit All four are testable in one line against a pristine shell (`zsh -f` skips rc files, so the result is the language's behaviour and not this machine's): ```zsh zsh -fc 'M=models/x; echo "$M:generateContent"; echo "${M}:generateContent"' ``` ```zsh zsh -fc 'v="a b"; set -- $v; echo "bare: 1=[$1] 2=[$2]"; set -- ${=v}; echo "split: 1=[$1] 2=[$2]"' ``` If the braced or `${=…}` form differs from the bare one, the expansion is the bug — not the tool you were calling.
GitHub에서 보기