Review Zsh scripts for Bash-isms and non-idiomatic patterns, then suggest or apply Zsh-native rewrites
Review Zsh scripts and rewrite non-idiomatic patterns into proper Zsh style. Detects Bash-isms, unnecessary external command usage, and missed Zsh-native features.
Activate when:
.zsh files in this repository| Avoid | Prefer | Why |
|---|---|---|
basename "$path" | ${path:t} | Zsh has :t (tail) modifier |
dirname "$path" | ${path:h} | Zsh has :h (head) modifier |
echo "$x" | tr A-Z a-z | ${(L)x} | Expansion flag |
echo "$x" | tr a-z A-Z | ${(U)x} | Expansion flag |
echo "$x" | sed 's/a/b/g' | ${x//a/b} | Pattern substitution |
echo "$x" | cut -d. -f1 | ${x%%.*} | Suffix removal |
echo "$x" | rev | ${(j::)${(@Oa)${(s::)x}}} | Only if simple; otherwise rev is fine |
echo "$x" | wc -l | ${(w)#${(f)x}} or ${#${(f)x}} | Line count via expansion |
echo "$x" | sort -u | ${(ou)${(f)x}} | Sort + unique flags |
echo "$x" | head -1 | ${${(f)x}[1]} | Array indexing |
expr $a + $b | $(( a + b )) | Arithmetic expansion |
seq 1 10 | {1..10} | Brace expansion |
cat file | $(<file) | Zsh file read syntax |
| Bash-ism | Zsh Equivalent | Notes |
|---|---|---|
${arr[0]} (first element) | ${arr[1]} | Zsh is 1-indexed |
declare -a | typeset -a | typeset is the Zsh convention |
declare -A | typeset -A | Same |
$BASH_REMATCH | $MATCH / $match | After =~ |
${!var} (indirect) | ${(P)var} | Parameter expansion flag |
${!arr[@]} (keys) | ${(k)arr} | Key expansion flag |
read -a arr | read -A arr | -A for array in Zsh |
[[ $x = *pattern* ]] | [[ $x == *pattern* ]] | == preferred in Zsh |
function foo { | foo() { | POSIX-style preferred; both work |
echo -e | print | print supports escapes natively |
echo -n | print -n | Or print -rn -- for safety |
source file | source file or . file | Both fine, but be consistent |
export VAR=val | export VAR=val or typeset -gx VAR=val | Both fine |
| Instead of | Prefer | Reason |
|---|---|---|
if [ -n "$(ls dir)" ] | if (( $#dir_files )); ... or glob qualifier | Avoid command substitution for file checks |
for f in $(ls *.txt) | for f in *.txt(N) | (N) prevents error on no match |
test -d "$d" && cd "$d" | cd "$d" 2>/dev/null or [[ -d $d ]] | [[ ]] preferred over test/[ ] |
cmd | while read line | while read line; do ...; done < <(cmd) or for line in ${(f)$(cmd)} | Avoid subshell from pipe |
local IFS=,; arr=($str) | arr=(${(s:,:)str}) | Split flag |
echo "$a" "$b" "$c" | print -r -- "$a" "$b" "$c" | print -r avoids escape interpretation |
VAR=$(echo $x | cmd1 | cmd2) | Chain expansions or single pipe | Reduce subshells |
[ $? -eq 0 ] | Direct if cmd; then | Check exit status directly |
grep -q pat file && ... | [[ $(<file) == *pat* ]] | For simple pattern checks on small files |
arr=($(echo $str)) | arr=(${=str}) | ${=var} does word splitting in Zsh |
wc -l < file | ${#${(f)"$(<file)"}} | Pure Zsh line count |
| Issue | Fix |
|---|---|
Unquoted $var in arguments | Usually safe in Zsh (no word splitting by default), but quote in [[ ]] RHS for literal match |
$(cmd) with word split intent | Use ${(f)$(cmd)} to split by lines, or ${=var} for word split |
Missing emulate -L zsh in public functions | Add for predictable behavior regardless of caller options |
Missing setopt LOCAL_OPTIONS | Use when temporarily changing options inside a function |
| Modifier | Meaning | Example |
|---|---|---|
:t | Tail (basename) | ${path:t} → file.txt |
:h | Head (dirname) | ${path:h} → /usr/local |
:r | Root (remove extension) | ${file:r} → archive.tar |
:e | Extension | ${file:e} → gz |
:l | Lowercase | ${var:l} |
:u | Uppercase | ${var:u} |
:a | Absolute path | ${path:a} |
:A | Absolute path with symlink resolution | ${path:A} |
:gs/x/y/ | Global substitution | ${path:gs/./_/} |
basename instead of :t)declare vs typeset)## Zsh Lint: <filename>
### Errors
- **L42**: `${arr[0]}` → `${arr[1]}` — Zsh arrays are 1-indexed
### Warnings
- **L15**: `basename "$path"` → `${path:t}` — use Zsh path modifier
- **L28**: `echo "$x" | tr A-Z a-z` → `${(L)x}` — use expansion flag
### Style
- **L3**: `declare -a` → `typeset -a` — Zsh convention
### Summary
3 issues found (1 error, 2 warnings, 1 style)
When the user asks to fix (not just lint), apply changes directly using the Edit tool.