| name | zsh |
| description | [Applies to: **/*] This guide defines the definitive best practices for writing Zsh scripts and configuration files, ensuring consistency, robustness, and maintainability across our projects. |
| source | cursor_mdc |
zsh Best Practices
Zsh is our default shell for interactive use and scripting. This guide outlines mandatory practices to ensure all Zsh code is robust, readable, and performant.
1. Core Principles
1.1 Interpreter & Strict Mode
Always specify zsh as the interpreter and enable strict mode for early error detection.
✅ GOOD:
#!/usr/bin/env zsh
setopt errreturn nounset pipefail
❌ BAD:
#!/bin/bash # Wrong interpreter
1.2 ShellCheck Integration
ShellCheck is non-negotiable. Integrate it into your editor and CI pipeline.
✅ GOOD:
name: ShellCheck
on: [push, pull_request]
jobs:
shellcheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run ShellCheck
uses: ludeeus/action-shellcheck@v2
with:
scandir: './'
2. Code Organization and Structure
2.1 File Header & Comments
Every script must start with a descriptive header. Functions require clear comments.
✅ GOOD:
#!/usr/bin/env zsh
function install_dependencies() {
}
2.2 Function Location & main
Define all functions before their first use. Use a main function for the script's entry point.
✅ GOOD:
#!/usr/bin/env zsh
setopt errreturn nounset pipefail
function _log_info() {
echo "[INFO] $*" >&2
}
function _process_data() {
_log_info "Processing data for: $1"
}
function main() {
_log_info "Starting data processing..."
_process_data "report_A"
_process_data "report_B"
_log_info "Processing complete."
}
main "$@"
3. Formatting
3.1 Indentation & Line Length
Indent with 2 spaces. Limit lines to 80 characters.
✅ GOOD:
if [[ -n "${VAR}" ]]; then
echo "Variable is set."
fi
long_command_name --option-one value \
--option-two another-value \
--final-option
❌ BAD:
if [[ -n "${VAR}" ]]; then
echo "Variable is set."
fi
long_command_name --option-one value --option-two another-value --final-option
3.2 Quoting & Variable Expansion
Always quote variables and command substitutions unless you explicitly need word splitting. Prefer ${parameter}.
✅ GOOD:
local my_file="report.txt"
local content="$(cat "${my_file}")"
echo "Processing file: ${my_file}"
❌ BAD:
local my_file=report.txt
local content=$(cat $my_file)
echo "Processing file: $my_file"
4. Naming Conventions
4.1 Functions & Variables
- Functions:
lower_snake_case. Prefix internal functions with _.
- Constants/Environment Variables:
ALL_CAPS.
- Local Variables:
local lower_snake_case.
✅ GOOD:
local MY_CONSTANT="fixed_value"
local _internal_helper_var="temp"
function calculate_total() {
local item_count="${1}"
local unit_price="${2}"
echo $(( item_count * unit_price ))
}
❌ BAD:
local myConstant="value"
function CalculateTotal() {
local ItemCount="${1}"
}
5. Zsh-Specific Patterns
5.1 Zsh Arrays
Use Zsh's native array handling. ${array} expands to all elements. Use "${(@)array}" to preserve empty entries.
✅ GOOD:
local my_array=("apple" "" "banana")
echo "All elements: ${my_array}"
echo "Preserving empty: ${(@)my_array}"
for item in "${(@)my_array}"; do
echo "Item: '${item}'"
done
❌ BAD:
local my_array=("apple" "" "banana")
echo "Bash style: ${my_array[@]}"
5.2 Extended Globbing
Enable extended_glob for powerful pattern matching.
✅ GOOD:
setopt extended_glob
ls -d *~*.(txt|log)
ls -d (#i)*.txt
5.3 Parameter Expansion for Path Manipulation
Avoid dirname and basename external commands.
✅ GOOD:
local full_path="/path/to/my/file.txt"
local dir_name="${full_path:h}"
local base_name="${full_path:t}"
local absolute_path="${full_path:A}"
❌ BAD:
local full_path="/path/to/my/file.txt"
local dir_name="$(dirname "${full_path}")"
local base_name="$(basename "${full_path}")"
5.4 Zsh-Native Filtering (Skipping grep/tr)
Leverage Zsh's parameter expansion for filtering and transformations.
✅ GOOD:
local lines=("line one" "another line" "third line")
local matched_lines=(${(M)lines:#*line*})
print -l "${matched_lines[@]}"
local text="hello world"
local transformed_text="${text//[aeiou]/_}"
❌ BAD:
local lines=("line one" "another line" "third line")
local matched_lines="$(printf "%s\n" "${lines[@]}" | grep "line")"
5.5 Ternary Expressions
Use :+ and :- for concise conditional assignments.
✅ GOOD:
local debug_mode="true"
local log_level="${debug_mode:+DEBUG}:INFO"
echo "Log level: ${log_level}"
local debug_mode=""
local log_level="${debug_mode:+DEBUG}:INFO"
echo "Log level: ${log_level}"
6. Anti-Patterns & Pitfalls
6.1 Avoid eval
eval is a security risk and makes code hard to debug. Never use it.
❌ BAD:
local cmd="ls -l"
eval "${cmd}"
6.2 source with Caution
Only source trusted files. For external tools, prefer explicit execution or eval "$(tool init zsh)" if the tool is trusted and designed for it.
✅ GOOD:
source "${ZDOTDIR:-$HOME}/.zsh_aliases"
eval "$(zoxide init zsh)"
❌ BAD:
source "/tmp/untrusted_script.zsh"
6.3 Minimal .zshrc
Keep your .zshrc lean. Avoid heavy frameworks like Oh My Zsh unless absolutely necessary. Prefer lightweight tools and manual configuration.
✅ GOOD:
HISTFILE=~/.zsh_history
HISTSIZE=100000
SAVEHIST=100000
setopt HIST_SAVE_NO_DUPS INC_APPEND_HISTORY AUTO_PUSHD PUSHD_IGNORE_DUPS PUSHD_SILENT autocd
autoload -U compinit; compinit
eval "$(starship init zsh)"
❌ BAD:
source $ZSH/oh-my-zsh.sh
plugins=(git docker web-search history-substring-search ...)