zsh

star 2

[Applies to: **/*] This guide defines the definitive best practices for writing Zsh scripts and configuration files, ensuring consistency, robustness, and maintainability across our projects.

Tryboy869 By Tryboy869 schedule Updated 2/28/2026

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 ...)
Install via CLI
npx skills add https://github.com/Tryboy869/dojutsu-for-ai --skill zsh
Repository Details
star Stars 2
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator