
Shell Integration
- 80 installs
- 14 repo stars
- Updated March 2, 2026
- oakoss/agent-skills
Helps with ai & agent building tasks during AI-assisted development.
About
shell-integration is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- shell-integration
- AI & Agent Building
- AI-coding skill
Shell Integration by the numbers
- 80 all-time installs (skills.sh)
- +3 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #5,222 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/oakoss/agent-skills --skill shell-integrationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 80 |
|---|---|
| repo stars | ★ 14 |
| Last updated | March 2, 2026 |
| Repository | oakoss/agent-skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Shell Integration
Overview
Shell integration covers the APIs and patterns for building tools that extend or interact with Unix shells. This includes completion systems, prompt hooks, key bindings, terminal control, and plugin distribution across Zsh, Bash, and Fish.
When to use: Building CLI tool completions, shell plugins, prompt customizations, terminal UI, dotfile managers, installation scripts, or native binary wrappers.
When NOT to use: General-purpose scripting unrelated to shell extension (use POSIX scripting reference for standalone scripts), GUI applications, or web server development.
Quick Reference
| Pattern | Shell | Key Points |
|---|---|---|
| Completion function | Zsh | compdef, compadd, zstyle for matcher configuration |
| Completion function | Bash | complete, compgen, COMP_WORDS, COMP_CWORD, COMPREPLY |
| Completion function | Fish | complete -c cmd -a args, condition flags, subcommand patterns |
| ZLE widget | Zsh | zle -N widget func, bindkey to map keys |
| Prompt hook | Zsh | precmd, preexec, chpwd via add-zsh-hook |
| Prompt hook | Bash | PROMPT_COMMAND (string or array in Bash 5.1+) |
| Event handler | Fish | --on-event, --on-variable, --on-signal |
| Abbreviation | Fish | abbr -a name expansion, --function for dynamic |
| Parameter expansion | Zsh | ${(s.:.)var}, ${var:=default}, flags and modifiers |
| Terminal control | All | ANSI/CSI escape sequences, tput, stty |
| Signal handling | All | trap builtin, cleanup patterns, EXIT/INT/TERM |
| Process management | All | Job control (&, wait, bg, fg), subshells, coprocesses |
| Plugin installation | All | Sourcing strategies, version detection, ZDOTDIR loading order |
Common Mistakes
| Mistake | Correct Pattern |
|---|---|
Using echo -e for escape sequences portably | Use printf or tput for portability across shells and OSes |
Modifying PROMPT_COMMAND with = in Bash | Append with += to avoid overwriting other tools |
| Defining Fish event handlers in lazy-loaded functions | Place event handlers in config.fish or source them explicitly |
| Hardcoding terminal capabilities | Query via tput which respects TERM and terminfo |
Missing emulate -L zsh in Zsh functions | Always set local options to avoid polluting caller environment |
Using $COMP_LINE splitting instead of COMP_WORDS | Use COMP_WORDS[$COMP_CWORD] for reliable word extraction |
Not quoting $@ in wrapper scripts | Always use "$@" to preserve argument boundaries |
Assuming /bin/sh is Bash | Target POSIX sh for portable scripts, test with dash |
Using function keyword in POSIX scripts | Use name() { ... } syntax for POSIX compatibility |
Ignoring EXIT trap for cleanup | Always set trap cleanup EXIT for temp files and state |
Delegation
- Completion testing: Use
Exploreagent to verify completions interactively - Script review: Use
Taskagent for cross-shell compatibility audits - Code review: Delegate to
code-revieweragent
If the rust skill is available, delegate native binary compilation patterns to it. Shell wrappers often invoke Rust-compiled binaries for performance-critical operations.If thecli-power-toolsskill is available, delegate modern CLI utility patterns to it. Many shell plugins wrap tools likefd,ripgrep, andfzf.
References
- Zsh integration: ZLE, completions, hooks, parameter expansion
- Bash integration: readline, completions, PROMPT_COMMAND, shopt
- Fish integration: completions, events, abbreviations, functions
- Terminal control: ANSI/CSI sequences, tput, stty, capabilities
- POSIX scripting: portable patterns, signal handling, process management
- Plugin distribution: installation scripts, dotfile management, version detection
Bash Integration
Readline Configuration
Readline controls line editing behavior in Bash. Configure via ~/.inputrc or bind builtin.
# ~/.inputrc
$if Bash
set show-all-if-ambiguous on
set completion-ignore-case on
set colored-stats on
set mark-symlinked-directories on
set show-all-if-unmodified on
set visible-stats on
$endif
# Vi mode
set editing-mode vi
# Custom bindings
"\C-r": reverse-search-history
"\C-p": history-search-backward
"\C-n": history-search-forward
"\e[A": history-search-backward
"\e[B": history-search-forwardBind Builtin
bind '"\C-x\C-r": re-read-init-file'
bind 'set show-all-if-ambiguous on'
bind -x '"\C-l": clear'
# List current bindings
bind -P | grep -v "not bound"Programmable Completion
Core Variables
| Variable | Purpose |
|---|---|
COMP_WORDS | Array of words on the command line |
COMP_CWORD | Index into COMP_WORDS for current word |
COMP_LINE | Full command line string |
COMP_POINT | Cursor position in COMP_LINE |
COMPREPLY | Array of completions to return |
Basic Completion Function
_mytool() {
local cur prev opts
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
opts="--help --verbose --config init build deploy"
case "$prev" in
--config)
COMPREPLY=($(compgen -f -X '!*.toml' -- "$cur"))
return 0
;;
esac
COMPREPLY=($(compgen -W "$opts" -- "$cur"))
}
complete -F _mytool mytoolCompgen Actions
# Complete with files
compgen -f -- "$cur"
# Complete with directories only
compgen -d -- "$cur"
# Complete with commands
compgen -c -- "$cur"
# Complete with variables
compgen -v -- "$cur"
# Complete with word list
compgen -W "start stop restart" -- "$cur"
# Complete with function output
compgen -W "$(mytool --list-commands 2>/dev/null)" -- "$cur"
# File extension filter
compgen -f -X '!*.json' -- "$cur"Subcommand Completion
_mytool() {
local cur prev words cword
_init_completion || return
if [[ $cword -eq 1 ]]; then
COMPREPLY=($(compgen -W "deploy config status" -- "$cur"))
return
fi
case "${words[1]}" in
deploy)
case "$prev" in
--env)
COMPREPLY=($(compgen -W "staging production" -- "$cur"))
;;
*)
COMPREPLY=($(compgen -W "--env --dry-run --force" -- "$cur"))
;;
esac
;;
config)
COMPREPLY=($(compgen -W "set get list" -- "$cur"))
;;
esac
}
complete -F _mytool mytoolUsing bash-completion Helpers
# _init_completion sets cur, prev, words, cword
_mytool() {
local cur prev words cword
_init_completion -n : || return
# _filedir completes files with optional extension filter
case "$prev" in
--config)
_filedir toml
;;
--output)
_filedir
;;
*)
COMPREPLY=($(compgen -W "--config --output --help" -- "$cur"))
;;
esac
}
complete -F _mytool mytoolPROMPT_COMMAND
Single Command (All Bash Versions)
_update_title() {
printf '\e]0;%s\a' "${PWD/#$HOME/~}"
}
PROMPT_COMMAND="_update_title"Array Form (Bash 5.1+)
# Append without overwriting other tools
PROMPT_COMMAND+=("_my_precmd")
_my_precmd() {
local exit_code=$?
history -a
_update_title
return $exit_code
}Safe Append (All Versions)
# Works with both string and array PROMPT_COMMAND
_append_prompt_command() {
if [[ "${BASH_VERSINFO[0]}" -ge 5 && "${BASH_VERSINFO[1]}" -ge 1 ]]; then
PROMPT_COMMAND+=("$1")
else
PROMPT_COMMAND="${PROMPT_COMMAND:+$PROMPT_COMMAND;} $1"
fi
}
_append_prompt_command "_my_precmd"Shopt Options
Useful Options for Plugins
# Glob patterns that match no files expand to null string
shopt -s nullglob
# ** matches directories recursively
shopt -s globstar
# Case-insensitive globbing
shopt -s nocaseglob
# Extended pattern matching: ?(pat), *(pat), +(pat), @(pat), !(pat)
shopt -s extglob
# Append to history instead of overwriting
shopt -s histappend
# Check window size after each command
shopt -s checkwinsize
# cd into directory by typing its name
shopt -s autocd
# Correct minor cd spelling errors
shopt -s cdspellChecking Options
if shopt -q globstar; then
echo "globstar is enabled"
fi
# Save and restore option state
local saved_nullglob
saved_nullglob=$(shopt -p nullglob)
shopt -s nullglob
# ... do work ...
eval "$saved_nullglob"Preexec Equivalent in Bash
Bash lacks a native preexec hook. The common pattern uses DEBUG trap:
_preexec_hook() {
# $BASH_COMMAND contains the command about to execute
if [[ "$BASH_COMMAND" != "$PROMPT_COMMAND" ]]; then
printf '\e]0;%s\a' "$BASH_COMMAND"
fi
}
trap '_preexec_hook' DEBUGBash Version Detection
if [[ "${BASH_VERSINFO[0]}" -lt 4 ]]; then
echo "Bash 4+ required for associative arrays" >&2
return 1
fi
if [[ "${BASH_VERSINFO[0]}" -eq 4 && "${BASH_VERSINFO[1]}" -lt 3 ]]; then
echo "Bash 4.3+ required for nameref" >&2
return 1
fiAssociative Arrays (Bash 4+)
declare -A config=(
[host]="localhost"
[port]="8080"
)
for key in "${!config[@]}"; do
printf '%s=%s\n' "$key" "${config[$key]}"
doneFish Integration
Configuration
Fish reads ~/.config/fish/config.fish on startup for interactive shells. Functions in ~/.config/fish/functions/ are autoloaded on first call.
~/.config/fish/
├── config.fish # Startup config (interactive)
├── conf.d/ # Drop-in config fragments (sourced alphabetically)
│ └── plugin.fish
├── functions/ # Autoloaded functions (one per file)
│ └── mytool.fish
└── completions/ # Autoloaded completions (one per command)
└── mytool.fishCompletion System
Basic Completion
# completions/mytool.fish
complete -c mytool -s h -l help -d "Show help"
complete -c mytool -s v -l verbose -d "Enable verbose output"
complete -c mytool -l config -r -F -d "Config file"
# Subcommands
complete -c mytool -n "__fish_use_subcommand" -a init -d "Initialize project"
complete -c mytool -n "__fish_use_subcommand" -a build -d "Build project"
complete -c mytool -n "__fish_use_subcommand" -a deploy -d "Deploy to production"Subcommand Options
# Options specific to "deploy" subcommand
complete -c mytool -n "__fish_seen_subcommand_from deploy" -l env -r -a "staging production" -d "Target environment"
complete -c mytool -n "__fish_seen_subcommand_from deploy" -l dry-run -d "Preview changes"
# Options specific to "config" subcommand
complete -c mytool -n "__fish_seen_subcommand_from config" -a "set get list" -d "Config action"Completion Flags Reference
| Flag | Purpose |
|---|---|
-c CMD | Command to complete for |
-s CHAR | Short option |
-l STRING | Long option |
-a ARGS | List of completions (space-separated or command substitution) |
-r | Option requires an argument |
-f | Do not complete with files |
-F | Complete with files (default, use to override -f) |
-n CONDITION | Only offer completion when condition is true |
-d DESC | Description for the completion |
-x | Shorthand for -r -f (exclusive, requires arg, no files) |
-k | Keep order of completions (do not sort) |
Dynamic Completions
complete -c mytool -n "__fish_seen_subcommand_from checkout" -a "(mytool list-branches 2>/dev/null)" -d "Branch"
# Custom condition function
function __mytool_needs_subcommand
set -l cmd (commandline -opc)
test (count $cmd) -eq 1
end
complete -c mytool -n "__mytool_needs_subcommand" -a "init build deploy"Built-in Condition Functions
__fish_use_subcommand # No subcommand typed yet
__fish_seen_subcommand_from CMD # Specific subcommand was typed
__fish_complete_directories # Complete directories
__fish_complete_users # Complete usernames
__fish_complete_groups # Complete group names
__fish_complete_pids # Complete process IDsEvent System
Named Events
function on_myevent --on-event myevent
echo "myevent fired with args: $argv"
end
emit myevent arg1 arg2Variable Watch Events
function on_pwd_change --on-variable PWD
echo "Changed to $PWD"
end
function on_path_change --on-variable fish_user_paths
echo "PATH updated"
endSignal Events
function on_winch --on-signal WINCH
echo "Terminal resized"
end
function cleanup --on-signal INT
echo "Caught SIGINT"
exit 1
endProcess Events
function notify_done --on-process-exit %self
echo "Shell exiting"
end
function job_done --on-job-exit (jobs -lp | tail -1)
echo "Background job completed"
endBuilt-in Events
| Event | Trigger |
|---|---|
fish_prompt | Before prompt display |
fish_preexec | Before command execution |
fish_postexec | After command execution |
fish_exit | Shell exit |
fish_cancel | Command line cancelled |
function log_command --on-event fish_preexec
echo (date +%T) $argv[1] >> ~/.fish_command_log
endAbbreviations
Basic Abbreviations
abbr -a gco "git checkout"
abbr -a gst "git status"
abbr -a ll "ls -la"Position-Aware Abbreviations
# Only expand at command position (not as argument)
abbr -a --position command g git
# Only expand as argument (not as command)
abbr -a --position anywhere !! "$history[1]"Dynamic Abbreviations (Function)
function _last_history_item
echo $history[1]
end
abbr -a !! --position anywhere --function _last_history_itemRegex Abbreviations
abbr -a dotdot --regex '^\.\.+$' --function _expand_dots
function _expand_dots
string repeat -n (math (string length -- $argv[1]) - 1) "../"
endFunctions
Autoloaded Function
# ~/.config/fish/functions/mytool.fish
function mytool -d "My custom tool"
switch $argv[1]
case init
echo "Initializing..."
case build
echo "Building..."
case '*'
echo "Usage: mytool {init|build}" >&2
return 1
end
endFunction with Options
function serve -d "Start dev server"
argparse h/help 'p/port=!_validate_int' -- $argv
or return
if set -q _flag_help
echo "Usage: serve [-p PORT]"
return 0
end
set -l port ($_flag_port; or echo 3000)
echo "Serving on port $port"
endUniversal Variables
Fish universal variables persist across sessions and sync between running instances:
# Set (persists and syncs across all fish sessions)
set -U EDITOR nvim
set -U fish_user_paths $HOME/.local/bin $fish_user_paths
# Remove
set -e -U fish_user_paths[1]Version Detection
if test (string match -r '\d+' $FISH_VERSION) -ge 4
echo "Fish 4.x features available"
end
set -l fish_major (string split . $FISH_VERSION)[1]
if test "$fish_major" -lt 3
echo "Fish 3+ required" >&2
return 1
endPlugin Distribution
Installation Script Pattern
#!/bin/sh
set -eu
INSTALL_DIR="${HOME}/.local/share/mytool"
BIN_DIR="${HOME}/.local/bin"
main() {
detect_platform
download_binary
install_shell_integration
print_instructions
}
detect_platform() {
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m)
case "$ARCH" in
x86_64|amd64) ARCH="x86_64" ;;
aarch64|arm64) ARCH="arm64" ;;
*) die "Unsupported architecture: $ARCH" ;;
esac
}
download_binary() {
local url="https://github.com/org/mytool/releases/latest/download/mytool-${OS}-${ARCH}"
mkdir -p "$BIN_DIR"
if command -v curl >/dev/null 2>&1; then
curl -fsSL "$url" -o "${BIN_DIR}/mytool"
elif command -v wget >/dev/null 2>&1; then
wget -qO "${BIN_DIR}/mytool" "$url"
else
die "curl or wget required"
fi
chmod +x "${BIN_DIR}/mytool"
}
install_shell_integration() {
mkdir -p "$INSTALL_DIR"
"${BIN_DIR}/mytool" --generate-completions zsh > "$INSTALL_DIR/completions.zsh" 2>/dev/null || true
"${BIN_DIR}/mytool" --generate-completions bash > "$INSTALL_DIR/completions.bash" 2>/dev/null || true
"${BIN_DIR}/mytool" --generate-completions fish > "$INSTALL_DIR/completions.fish" 2>/dev/null || true
}
print_instructions() {
cat <<'EOF'
Installation complete!
Add to your shell config:
# Bash (~/.bashrc)
eval "$(mytool init bash)"
# Zsh (~/.zshrc)
eval "$(mytool init zsh)"
# Fish (~/.config/fish/config.fish)
mytool init fish | source
EOF
}
die() {
printf 'Error: %s\n' "$1" >&2
exit 1
}
main "$@"Shell Init Command Pattern
Many tools generate shell integration via a tool init <shell> command:
# The init command outputs shell code to stdout
mytool_init_zsh() {
cat <<'INIT'
_mytool_hook() {
emulate -L zsh
eval "$(mytool hook zsh)"
}
add-zsh-hook precmd _mytool_hook
if [[ -f "${MYTOOL_DIR}/completions.zsh" ]]; then
source "${MYTOOL_DIR}/completions.zsh"
fi
INIT
}
mytool_init_bash() {
cat <<'INIT'
_mytool_hook() {
eval "$(mytool hook bash)"
}
PROMPT_COMMAND="${PROMPT_COMMAND:+$PROMPT_COMMAND;} _mytool_hook"
if [[ -f "${MYTOOL_DIR}/completions.bash" ]]; then
source "${MYTOOL_DIR}/completions.bash"
fi
INIT
}
mytool_init_fish() {
cat <<'INIT'
function _mytool_hook --on-event fish_prompt
mytool hook fish | source
end
if test -f "$MYTOOL_DIR/completions.fish"
source "$MYTOOL_DIR/completions.fish"
end
INIT
}Sourcing Strategies
Direct Sourcing
# Zsh: simple but runs every shell startup
source "$HOME/.local/share/mytool/init.zsh"Lazy Loading (Zsh)
Defer loading until the command is first used:
mytool() {
unfunction mytool
eval "$(command mytool init zsh)"
mytool "$@"
}Cached Eval
Cache the init output to avoid running the binary on every shell startup:
_mytool_cache="$HOME/.cache/mytool/init.zsh"
if [ ! -f "$_mytool_cache" ] || [ "$(mytool --version 2>/dev/null)" != "$(head -1 "$_mytool_cache" 2>/dev/null)" ]; then
mkdir -p "${_mytool_cache%/*}"
{
mytool --version
mytool init zsh
} > "$_mytool_cache" 2>/dev/null
fi
source "$_mytool_cache"Compile Zsh Scripts
# Compile for faster loading
if [[ ! -f "${init_script}.zwc" ]] || [[ "$init_script" -nt "${init_script}.zwc" ]]; then
zcompile "$init_script"
fi
source "$init_script"Shell Detection
detect_shell() {
local shell_name
shell_name=$(basename "$SHELL")
case "$shell_name" in
zsh) echo "zsh" ;;
bash) echo "bash" ;;
fish) echo "fish" ;;
*) echo "unknown" ;;
esac
}
# More reliable: check current running shell
detect_current_shell() {
if [ -n "${ZSH_VERSION:-}" ]; then
echo "zsh"
elif [ -n "${BASH_VERSION:-}" ]; then
echo "bash"
elif [ -n "${FISH_VERSION:-}" ]; then
echo "fish"
else
echo "sh"
fi
}Version Detection and Feature Gating
# Zsh version check
check_zsh_version() {
if [ -n "${ZSH_VERSION:-}" ]; then
autoload -Uz is-at-least
if ! is-at-least 5.2; then
echo "Warning: Zsh 5.2+ recommended" >&2
return 1
fi
fi
}
# Bash version check
check_bash_version() {
if [ -n "${BASH_VERSION:-}" ]; then
local major="${BASH_VERSINFO[0]}"
local minor="${BASH_VERSINFO[1]}"
if [ "$major" -lt 4 ] || { [ "$major" -eq 4 ] && [ "$minor" -lt 0 ]; }; then
echo "Warning: Bash 4+ recommended" >&2
return 1
fi
fi
}
# Feature gating
setup_completions() {
if [ -n "${ZSH_VERSION:-}" ]; then
setup_zsh_completions
elif [ -n "${BASH_VERSION:-}" ]; then
if [ "${BASH_VERSINFO[0]}" -ge 4 ]; then
setup_bash4_completions
else
setup_bash3_completions
fi
fi
}Dotfile Management
XDG Base Directory Compliance
config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/mytool"
data_dir="${XDG_DATA_HOME:-$HOME/.local/share}/mytool"
cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}/mytool"
state_dir="${XDG_STATE_HOME:-$HOME/.local/state}/mytool"
mkdir -p "$config_dir" "$data_dir" "$cache_dir" "$state_dir"Safe Config File Modification
add_to_shell_config() {
local config_file="$1"
local init_line="$2"
local marker="# mytool"
if [ ! -f "$config_file" ]; then
return 1
fi
if grep -qF "$marker" "$config_file" 2>/dev/null; then
return 0
fi
printf '\n%s\n%s\n' "$marker" "$init_line" >> "$config_file"
}
install_to_shell() {
local shell
shell=$(detect_shell)
case "$shell" in
zsh)
add_to_shell_config "${ZDOTDIR:-$HOME}/.zshrc" 'eval "$(mytool init zsh)"'
;;
bash)
add_to_shell_config "$HOME/.bashrc" 'eval "$(mytool init bash)"'
;;
fish)
local fish_conf="${XDG_CONFIG_HOME:-$HOME/.config}/fish/conf.d"
mkdir -p "$fish_conf"
mytool init fish > "$fish_conf/mytool.fish"
;;
esac
}Uninstallation
uninstall() {
rm -f "${BIN_DIR}/mytool"
rm -rf "${INSTALL_DIR}"
# Remove from shell configs
for rc in "$HOME/.bashrc" "$HOME/.zshrc" "${ZDOTDIR:-$HOME}/.zshrc"; do
if [ -f "$rc" ]; then
sed -i.bak '/# mytool/d;/mytool init/d' "$rc"
rm -f "${rc}.bak"
fi
done
# Remove fish config
rm -f "${XDG_CONFIG_HOME:-$HOME/.config}/fish/conf.d/mytool.fish"
rm -f "${XDG_CONFIG_HOME:-$HOME/.config}/fish/completions/mytool.fish"
}Plugin Manager Integration
Oh My Zsh
# Place in ~/.oh-my-zsh/custom/plugins/mytool/
mytool/
├── mytool.plugin.zsh # Main entry point (auto-sourced)
└── _mytool # Completion file (added to fpath)# mytool.plugin.zsh
if (( ! $+commands[mytool] )); then
return
fi
eval "$(mytool init zsh)"Fisher (Fish)
# Repository structure for Fisher
mytool/
├── conf.d/
│ └── mytool.fish # Auto-sourced config
├── completions/
│ └── mytool.fish # Auto-loaded completions
└── functions/
└── mytool_helper.fish # Auto-loaded functionsZinit / Sheldon / Antidote (Zsh)
# Zinit
zinit light org/mytool-zsh
# Sheldon (sheldon.toml)
# [plugins.mytool]
# github = "org/mytool-zsh"
# Antidote
# org/mytool-zshGraceful Degradation
init_plugin() {
if ! command -v mytool >/dev/null 2>&1; then
_mytool_not_found() {
echo "mytool not found. Install: https://mytool.dev/install" >&2
return 127
}
alias mytool='_mytool_not_found'
return
fi
local version
version=$(mytool --version 2>/dev/null | head -1)
case "$version" in
1.*)
eval "$(mytool init-v1 "$current_shell")"
;;
2.*|3.*)
eval "$(mytool init "$current_shell")"
;;
*)
echo "Warning: Unknown mytool version ($version), attempting init" >&2
eval "$(mytool init "$current_shell" 2>/dev/null)" || true
;;
esac
}POSIX Scripting
Portable Shebang
#!/bin/shUse /bin/sh for POSIX scripts. Avoid #!/bin/bash unless Bash features are required.
POSIX-Compatible Constructs
Conditionals
# POSIX test (use [ ] not [[ ]])
if [ "$var" = "value" ]; then
echo "match"
fi
# String checks
[ -n "$var" ] # Non-empty
[ -z "$var" ] # Empty
[ "$a" = "$b" ] # Equal (single =, not ==)
[ "$a" != "$b" ] # Not equal
# Numeric comparison
[ "$a" -eq "$b" ] # Equal
[ "$a" -lt "$b" ] # Less than
[ "$a" -gt "$b" ] # Greater than
# File tests
[ -f "$path" ] # Regular file exists
[ -d "$path" ] # Directory exists
[ -r "$path" ] # Readable
[ -w "$path" ] # Writable
[ -x "$path" ] # Executable
[ -s "$path" ] # Non-empty file
[ -L "$path" ] # Symbolic linkFunctions (POSIX Syntax)
# POSIX: no "function" keyword
my_func() {
local var="$1"
echo "$var"
return 0
}String Operations (No Bashisms)
# Parameter expansion (POSIX)
${var#pattern} # Remove shortest prefix match
${var##pattern} # Remove longest prefix match
${var%pattern} # Remove shortest suffix match
${var%%pattern} # Remove longest suffix match
# Extract filename
filename="${path##*/}"
# Extract directory
dirname="${path%/*}"
# Extract extension
ext="${filename##*.}"
# Remove extension
base="${filename%.*}"Loops
# Iterate over arguments
for arg in "$@"; do
echo "$arg"
done
# C-style not POSIX; use while instead
i=0
while [ "$i" -lt 10 ]; do
echo "$i"
i=$((i + 1))
done
# Read lines from file
while IFS= read -r line; do
echo "$line"
done < file.txtSignal Handling
Trap Syntax
trap 'handler_commands' SIGNAL_LIST
# Common signals
trap 'cleanup' EXIT # Always runs on exit
trap 'cleanup; exit 1' INT # Ctrl-C
trap 'cleanup; exit 1' TERM # kill (default signal)
trap '' HUP # Ignore hangup
trap 'reload_config' USR1 # Custom: reloadCleanup Pattern
cleanup() {
rm -f "$tmpfile"
[ -n "$pid" ] && kill "$pid" 2>/dev/null
}
trap cleanup EXIT
tmpfile=$(mktemp)Signal Reference
| Signal | Number | Default | Common Use |
|---|---|---|---|
HUP | 1 | Terminate | Reload config, terminal closed |
INT | 2 | Terminate | Ctrl-C |
QUIT | 3 | Core dump | Ctrl-\\ |
TERM | 15 | Terminate | Graceful shutdown request |
USR1 | 10 | Terminate | Custom (reload, log rotate) |
USR2 | 12 | Terminate | Custom |
PIPE | 13 | Terminate | Broken pipe |
WINCH | 28 | Ignore | Terminal resize |
EXIT | N/A | N/A | Shell exit (trap-only pseudo-signal) |
Graceful Shutdown
shutdown_requested=0
handle_term() {
shutdown_requested=1
}
trap handle_term TERM INT
while [ "$shutdown_requested" -eq 0 ]; do
do_work
sleep 1
done
cleanupProcess Management
Background Processes
long_task &
bg_pid=$!
# Wait for specific process
wait "$bg_pid"
exit_code=$?
# Wait for all background jobs
waitParallel Execution with Limits
max_jobs=4
running=0
for item in "$@"; do
process_item "$item" &
running=$((running + 1))
if [ "$running" -ge "$max_jobs" ]; then
wait -n 2>/dev/null || wait
running=$((running - 1))
fi
done
waitPID File Management
pidfile="/var/run/myservice.pid"
acquire_lock() {
if [ -f "$pidfile" ]; then
local old_pid
old_pid=$(cat "$pidfile")
if kill -0 "$old_pid" 2>/dev/null; then
echo "Already running (PID $old_pid)" >&2
return 1
fi
rm -f "$pidfile"
fi
echo $$ > "$pidfile"
}
release_lock() {
rm -f "$pidfile"
}
trap release_lock EXIT
acquire_lock || exit 1Subshell Isolation
# Changes inside subshell do not affect parent
(
cd /tmp || exit 1
export MY_VAR="local"
do_work
)
# PWD and MY_VAR unchanged hereIPC Patterns
Named Pipes (FIFOs)
fifo="/tmp/myfifo.$$"
mkfifo "$fifo"
trap 'rm -f "$fifo"' EXIT
# Writer
echo "message" > "$fifo" &
# Reader
while IFS= read -r msg; do
echo "Received: $msg"
done < "$fifo"Unix Socket IPC from Shell
# Send command via socat
echo '{"command":"status"}' | socat - UNIX-CONNECT:/tmp/myapp.sock
# Listen on socket (simple server)
socat UNIX-LISTEN:/tmp/myapp.sock,fork EXEC:./handler.shHere Document for Input
command <<'HEREDOC'
Multi-line input
No variable expansion with quoted delimiter
HEREDOC
command <<HEREDOC
Expands $VARIABLES
And $(commands)
HEREDOCPortable Patterns
Safe Temporary Files
tmpdir=$(mktemp -d) || exit 1
trap 'rm -rf "$tmpdir"' EXIT
tmpfile="$tmpdir/work"Command Existence Check
has_cmd() {
command -v "$1" >/dev/null 2>&1
}
if has_cmd curl; then
curl -fsSL "$url"
elif has_cmd wget; then
wget -qO- "$url"
else
echo "curl or wget required" >&2
exit 1
fiStrict Mode
set -eu
# -e: exit on error
# -u: error on undefined variable
# For pipelines (not POSIX but widely supported)
set -o pipefail 2>/dev/null || trueOS Detection
detect_os() {
case "$(uname -s)" in
Linux*) echo "linux" ;;
Darwin*) echo "macos" ;;
MINGW*|MSYS*|CYGWIN*) echo "windows" ;;
FreeBSD*) echo "freebsd" ;;
*) echo "unknown" ;;
esac
}
detect_arch() {
case "$(uname -m)" in
x86_64|amd64) echo "x86_64" ;;
aarch64|arm64) echo "arm64" ;;
armv7*) echo "armv7" ;;
*) echo "unknown" ;;
esac
}Logging
log_info() { printf '[INFO] %s\n' "$*" >&2; }
log_warn() { printf '[WARN] %s\n' "$*" >&2; }
log_error() { printf '[ERROR] %s\n' "$*" >&2; }
die() {
log_error "$@"
exit 1
}Terminal Control
ANSI/CSI Escape Sequences
CSI (Control Sequence Introducer) sequences start with ESC [ (\e[ or \033[).
Cursor Movement
printf '\e[H' # Move to home position (1,1)
printf '\e[%d;%dH' 5 10 # Move to row 5, column 10
printf '\e[A' # Up one line
printf '\e[B' # Down one line
printf '\e[C' # Forward one column
printf '\e[D' # Back one column
printf '\e[%dA' 3 # Up 3 lines
printf '\e[s' # Save cursor position
printf '\e[u' # Restore cursor position
printf '\e[6n' # Query cursor position (response: ESC[row;colR)Screen Control
printf '\e[2J' # Clear entire screen
printf '\e[0J' # Clear from cursor to end of screen
printf '\e[1J' # Clear from start of screen to cursor
printf '\e[2K' # Clear entire line
printf '\e[0K' # Clear from cursor to end of line
printf '\e[1K' # Clear from start of line to cursorText Attributes
printf '\e[0m' # Reset all attributes
printf '\e[1m' # Bold
printf '\e[2m' # Dim
printf '\e[3m' # Italic
printf '\e[4m' # Underline
printf '\e[7m' # Reverse (swap fg/bg)
printf '\e[8m' # Hidden
printf '\e[9m' # StrikethroughColors (SGR)
# Standard colors (foreground: 30-37, background: 40-47)
printf '\e[31m' # Red foreground
printf '\e[42m' # Green background
printf '\e[1;34m' # Bold blue
# Bright colors (foreground: 90-97, background: 100-107)
printf '\e[91m' # Bright red
# 256-color mode
printf '\e[38;5;%dm' 208 # Foreground: color 208 (orange)
printf '\e[48;5;%dm' 236 # Background: color 236 (dark gray)
# True color (24-bit)
printf '\e[38;2;%d;%d;%dm' 255 128 0 # Foreground RGB
printf '\e[48;2;%d;%d;%dm' 30 30 30 # Background RGBColor Utility Functions
color_fg() {
printf '\e[38;5;%dm' "$1"
}
color_bg() {
printf '\e[48;5;%dm' "$1"
}
rgb_fg() {
printf '\e[38;2;%d;%d;%dm' "$1" "$2" "$3"
}
reset_color() {
printf '\e[0m'
}Tput (Portable Terminal Control)
tput queries the terminfo database and outputs the correct escape sequences for the current terminal.
Common Capabilities
tput cols # Number of columns
tput lines # Number of lines
tput colors # Number of supported colors
tput cup 5 10 # Move cursor to row 5, col 10
tput home # Move to home position
tput sc # Save cursor
tput rc # Restore cursor
tput clear # Clear screen
tput el # Clear to end of line
tput el1 # Clear to beginning of line
tput ed # Clear to end of screen
tput bold # Bold
tput dim # Dim
tput smul # Start underline
tput rmul # End underline
tput rev # Reverse video
tput sgr0 # Reset all attributes
tput setaf 1 # Set foreground color (0-255)
tput setab 2 # Set background color (0-255)
tput civis # Hide cursor
tput cnorm # Show cursor (normal)
tput smcup # Enter alternate screen
tput rmcup # Exit alternate screenAlternate Screen Buffer
enter_fullscreen() {
tput smcup
tput civis
tput clear
}
exit_fullscreen() {
tput rmcup
tput cnorm
}
# Ensure cleanup on exit
trap exit_fullscreen EXIT
enter_fullscreenFeature Detection
has_color() {
local colors
colors=$(tput colors 2>/dev/null) || return 1
[ "$colors" -ge 8 ]
}
has_truecolor() {
case "$COLORTERM" in
truecolor|24bit) return 0 ;;
esac
return 1
}
supports_unicode() {
case "$LANG$LC_ALL$LC_CTYPE" in
*UTF-8*|*utf-8*|*utf8*) return 0 ;;
esac
return 1
}Stty (Terminal Line Settings)
# Save current settings
saved_stty=$(stty -g)
# Restore settings
stty "$saved_stty"
# Raw mode (no echo, no line buffering)
stty raw -echo
# Read single character
read_char() {
local old_stty
old_stty=$(stty -g)
stty raw -echo min 1
local char
char=$(dd bs=1 count=1 2>/dev/null)
stty "$old_stty"
printf '%s' "$char"
}
# Disable Ctrl-C (SIGINT)
stty -isig
# Disable flow control (Ctrl-S/Ctrl-Q)
stty -ixonTerminal Queries
Window Size
# Via stty
stty size # rows cols
# Via tput
rows=$(tput lines)
cols=$(tput cols)
# Via SIGWINCH handler
handle_resize() {
LINES=$(tput lines)
COLUMNS=$(tput cols)
}
trap handle_resize WINCHCursor Position
get_cursor_pos() {
local pos
printf '\e[6n'
IFS='[;' read -r -d R _ row col < /dev/tty
printf '%d %d' "$row" "$col"
}Terminal Title
# Set title (works in most terminal emulators)
set_title() {
printf '\e]0;%s\a' "$1"
}
# Set title with icon name separately
set_icon_title() {
printf '\e]1;%s\a' "$1" # Icon name
printf '\e]2;%s\a' "$2" # Window title
}Progress Indicators
Spinner
spinner() {
local pid=$1
local frames='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
local i=0
while kill -0 "$pid" 2>/dev/null; do
printf '\r%s %s' "${frames:i%${#frames}:1}" "$2"
i=$((i + 1))
sleep 0.1
done
printf '\r\e[2K'
}
long_running_task &
spinner $! "Processing..."Progress Bar
progress_bar() {
local current=$1 total=$2 width=${3:-40}
local pct=$((current * 100 / total))
local filled=$((current * width / total))
local empty=$((width - filled))
printf '\r['
printf '%*s' "$filled" '' | tr ' ' '#'
printf '%*s' "$empty" '' | tr ' ' '-'
printf '] %3d%%' "$pct"
}
for i in $(seq 1 100); do
progress_bar "$i" 100
sleep 0.05
done
printf '\n'OSC (Operating System Command) Sequences
# Hyperlink (supported by many modern terminals)
printf '\e]8;;https://example.com\e\\Click here\e]8;;\e\\'
# Set clipboard (OSC 52)
printf '\e]52;c;%s\a' "$(printf '%s' "text" | base64)"
# Desktop notification (iTerm2, Kitty)
printf '\e]9;%s\a' "Task complete"Zsh Integration
ZDOTDIR Loading Order
Zsh reads startup files in a specific order. Understanding this is critical for plugin installation.
/etc/zshenv -> $ZDOTDIR/.zshenv (always, every shell)
/etc/zprofile -> $ZDOTDIR/.zprofile (login shells only)
/etc/zshrc -> $ZDOTDIR/.zshrc (interactive shells only)
/etc/zlogin -> $ZDOTDIR/.zlogin (login shells only, after .zshrc)
/etc/zlogout -> $ZDOTDIR/.zlogout (login shells only, on exit)ZDOTDIR defaults to $HOME if unset. Plugins targeting interactive use belong in .zshrc.
ZLE Widgets
ZLE (Zsh Line Editor) widgets are functions bound to key sequences for custom line editing behavior.
my-widget() {
emulate -L zsh
BUFFER="modified: $BUFFER"
CURSOR=$#BUFFER
}
zle -N my-widget
bindkey '^X^M' my-widgetKey ZLE variables available inside widgets:
$BUFFER # Full command line contents
$LBUFFER # Text left of cursor
$RBUFFER # Text right of cursor
$CURSOR # Cursor position (0-indexed)
$WIDGET # Name of the widget being executed
$KEYMAP # Current keymap name
$KEYS # Keys that invoked the widgetAccept-Line Wrapper
Intercept Enter to add validation or transformation before execution:
custom-accept-line() {
emulate -L zsh
if [[ "$BUFFER" == *"rm -rf /"* ]]; then
zle -M "Blocked dangerous command"
return 1
fi
zle .accept-line
}
zle -N accept-line custom-accept-lineWidget with Completion
fzf-history-widget() {
emulate -L zsh
setopt localoptions pipefail
local selected
selected=$(fc -rln 1 | fzf --height 40% --reverse)
if [[ -n "$selected" ]]; then
BUFFER="$selected"
CURSOR=$#BUFFER
fi
zle reset-prompt
}
zle -N fzf-history-widget
bindkey '^R' fzf-history-widgetCompletion System
Basic Completion Function
#compdef mytool
_mytool() {
local -a commands
commands=(
'init:Initialize a new project'
'build:Build the project'
'deploy:Deploy to production'
)
_arguments \
'(-h --help)'{-h,--help}'[Show help]' \
'(-v --verbose)'{-v,--verbose}'[Enable verbose output]' \
'--config[Config file]:file:_files -g "*.toml"' \
'1:command:->cmd'
case "$state" in
cmd)
_describe 'command' commands
;;
esac
}
_mytoolSubcommand Completion
#compdef mytool
_mytool() {
local curcontext="$curcontext" state line
typeset -A opt_args
_arguments -C \
'1:command:->cmd' \
'*::arg:->args'
case "$state" in
cmd)
local -a commands=(
'deploy:Deploy the application'
'config:Manage configuration'
)
_describe 'command' commands
;;
args)
case "${line[1]}" in
deploy)
_arguments \
'--env[Target environment]:env:(staging production)' \
'--dry-run[Preview changes]'
;;
config)
_arguments \
'set:Set a value' \
'get:Get a value'
;;
esac
;;
esac
}
_mytoolCompadd Direct Usage
For low-level control over completion candidates:
_mytool_branches() {
local -a branches
branches=(${(f)"$(git branch --format='%(refname:short)' 2>/dev/null)"})
compadd -V branches -d branches -- "${branches[@]}"
}Zstyle Configuration
# Case-insensitive matching
zstyle ':completion:*' matcher-list 'm:{a-zA-Z}={A-Za-z}'
# Group completions by category
zstyle ':completion:*' group-name ''
zstyle ':completion:*:descriptions' format '%B%d%b'
# Cache completions for expensive operations
zstyle ':completion:*' use-cache on
zstyle ':completion:*' cache-path "$HOME/.zcompcache"
# Menu selection with highlighting
zstyle ':completion:*' menu select
zstyle ':completion:*:default' list-colors ${(s.:.)LS_COLORS}Hooks
Using add-zsh-hook
autoload -Uz add-zsh-hook
my_precmd() {
# Runs before each prompt display
print -Pn "\e]0;%~\a"
}
add-zsh-hook precmd my_precmd
my_preexec() {
# Runs after command is read, before execution
# $1 = the command string as typed
# $2 = single-line expanded version
# $3 = full expanded command
timer_start=$EPOCHSECONDS
}
add-zsh-hook preexec my_preexec
my_chpwd() {
# Runs when working directory changes
ls
}
add-zsh-hook chpwd my_chpwdAvailable Hook Points
| Hook | Trigger | Common Use |
|---|---|---|
precmd | Before prompt display | Update prompt, set title |
preexec | After command read, before exec | Start timer, log command |
chpwd | Directory change | Auto-ls, update env |
periodic | Every $PERIOD seconds | Background checks |
zshaddhistory | Before history write | Filter sensitive commands |
zshexit | Shell exit | Cleanup |
Bindkey and Keymaps
# List current bindings
bindkey -L
# Bind in specific keymap
bindkey -M viins '^A' beginning-of-line
bindkey -M vicmd 'k' up-line-or-history
# Create custom keymap
bindkey -N mymap
bindkey -M mymap '^X' my-widget
# Common key sequences
bindkey '^[[A' up-line-or-search # Up arrow
bindkey '^[[B' down-line-or-search # Down arrow
bindkey '^[[3~' delete-char # Delete keyParameter Expansion
Flags
str="hello:world:foo"
# Split on delimiter
print -l ${(s.:.)str} # hello\nworld\nfoo
# Join array
arr=(hello world foo)
print ${(j.:.)arr} # hello:world:foo
# Uppercase / lowercase
print ${(U)str} # HELLO:WORLD:FOO
print ${(L)str} # hello:world:foo
# Length
print ${#str} # 15
# Unique elements
arr=(a b a c b)
print ${(u)arr} # a b cModifiers
path="/home/user/file.tar.gz"
print ${path:h} # /home/user (head/dirname)
print ${path:t} # file.tar.gz (tail/basename)
print ${path:r} # /home/user/file.tar (remove extension)
print ${path:e} # gz (extension)
print ${path:A} # /home/user/file.tar.gz (absolute path)Defaults and Substitution
# Default if unset
print ${var:-default}
# Assign default if unset
: ${var:=default}
# Error if unset
: ${var:?'var must be set'}
# Substitute if set
print ${var:+is_set}
# Pattern substitution
str="hello world"
print ${str/world/zsh} # hello zsh
print ${str//o/0} # hell0 w0rldEmulate for Safety
Always use emulate -L zsh at the top of plugin functions to ensure consistent behavior regardless of the user's option settings:
my_plugin_func() {
emulate -L zsh
setopt extended_glob no_unset pipe_fail
# Function body with known option state
}