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
# Enable strict mode:
# errreturn: Exit immediately if a command exits with a non-zero status.
# nounset: Treat unset variables as an error.
# pipefail: The exit status of a pipeline is the status of the last command to exit with a non-zero status, or zero if all commands exit successfully.
setopt errreturn nounset pipefail
❌ BAD:
#!/bin/bash # Wrong interpreter
# No strict mode options
1.2 ShellCheck Integration
ShellCheck is non-negotiable. Integrate it into your editor and CI pipeline.
✅ GOOD:
# .github/workflows/ci.yml
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: './' # Scan all shell scripts in the repository
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
#
# Description: Manages project dependencies via a custom tool.
#
# setopt errreturn nounset pipefail
#######################################
# Fetches and installs project dependencies.
# Globals:
# PROJECT_ROOT
# Arguments:
# None
# Outputs:
# Writes status messages to stderr.
# Returns:
# 0 if dependencies are installed, non-zero on error.
#######################################
function install_dependencies() {
# ... implementation ...
}
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 # 4 spaces
long_command_name --option-one value --option-two another-value --final-option # Too long
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}")" # Command substitution quoted
echo "Processing file: ${my_file}" # Variable quoted, preferred syntax
❌ BAD:
local my_file=report.txt
local content=$(cat $my_file) # Unquoted, prone to word splitting/globbing
echo "Processing file: $my_file" # Legacy syntax
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" # CamelCase
function CalculateTotal() { # PascalCase
local ItemCount="${1}" # PascalCase
}
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}" # apple banana (empty removed)
echo "Preserving empty: ${(@)my_array}" # apple banana (empty preserved)
# Iterate safely
for item in "${(@)my_array}"; do
echo "Item: '${item}'"
done
❌ BAD:
local my_array=("apple" "" "banana")
echo "Bash style: ${my_array[@]}" # Works, but not idiomatic Zsh
5.2 Extended Globbing
Enable extended_glob for powerful pattern matching.
✅ GOOD:
setopt extended_glob
# Find files not ending with .txt or .log
ls -d *~*.(txt|log)
# Case-insensitive glob
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}" # /path/to/my
local base_name="${full_path:t}" # file.txt
local absolute_path="${full_path:A}" # Resolve symlinks and get absolute path
❌ 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:
# Filter array (like grep)
local lines=("line one" "another line" "third line")
local matched_lines=(${(M)lines:#*line*}) # Matches lines containing "line"
print -l "${matched_lines[@]}"
# Transform string (like tr)
local text="hello world"
local transformed_text="${text//[aeiou]/_}" # h_ll_ w_rld
❌ BAD:
local lines=("line one" "another line" "third line")
local matched_lines="$(printf "%s\n" "${lines[@]}" | grep "line")" # External command, subshell
5.5 Ternary Expressions
Use :+ and :- for concise conditional assignments.
✅ GOOD:
local debug_mode="true"
local log_level="${debug_mode:+DEBUG}:INFO" # If debug_mode is set, prepend DEBUG
echo "Log level: ${log_level}" # Output: Log level: DEBUG:INFO
local debug_mode=""
local log_level="${debug_mode:+DEBUG}:INFO"
echo "Log level: ${log_level}" # Output: Log level: :INFO
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}" # Dangerous!
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:
# .zshrc
# Source trusted local configuration
source "${ZDOTDIR:-$HOME}/.zsh_aliases"
# For trusted tools that provide init scripts
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:
# .zshrc
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
# Prompt via Starship (external, lightweight)
eval "$(starship init zsh)"
❌ BAD:
# .zshrc
# Too many plugins, slow startup
source $ZSH/oh-my-zsh.sh
plugins=(git docker web-search history-substring-search ...)