Files
oam/knowledge base/zsh.md

16 KiB

ZSH

Table of contents

  1. TL;DR
  2. Alias expansion
  3. Parameter expansion
    1. Parameter substitution
      1. Check if a variable is set
      2. Provide a default value
      3. Just substitute with its value if set
      4. Set a default value and substitute
      5. Fail on missing value
    2. Matching and replacement
  4. Arrays
  5. Tests
  6. Find broken symlinks in the current directory
  7. Key bindings
  8. Configuration
    1. Config files read order
    2. History
    3. Completion
    4. Prompt management
    5. Automatic source of files in a folder
  9. Frameworks
  10. Plugins
  11. Troubleshooting
    1. The delete, end and/or home keys are not working as intended
    2. Compinit warnings of insecure directories and files
  12. Further readings

TL;DR

Startup files load sequence:

  1. /etc/zshenv
  2. ${ZDOTDIR}/.zshenv
  3. login shells only:
    1. /etc/zprofile
    2. ${ZDOTDIR}/.zprofile
  4. interactive shells only:
    1. /etc/zshrc
    2. ${ZDOTDIR}/.zshrc
  5. login shells only:
    1. /etc/zlogin
    2. ${ZDOTDIR}/.zlogin

Exit files load sequence:

  1. /etc/zlogout
  2. ${ZDOTDIR}/.zlogout

Aliases are expanded when the function definition is parsed, not when the function is executed. Define aliases before functions to avoid problems.

# Logout after 3 minutes of inactivity.
TMOUT=180

# Quoting.
"$scalar"
"${array[@]}"
"${(@)array}"

# Create a function.
function_name () {}
function function_name {}
function function_name () {}

# Regex match.
[[ "$OSTYPE" =~ "darwin" ]]
[[ "$OSTYPE" -regex-match "darwin" ]]

# Find broken symlinks in the current directory.
ls **/*(-@)

# Print all shell and environment variables.
setopt posixbuiltins && set

# Treat '#' as a comment starter instead of matching patterns.
# Disabled by default in interactive sessions, enabled by default in
# non-interactive ones.
setopt interactive_comments
shopt -u interactive_comments

# Print exported variables only.
export -p

# Make entries unique in an array.
typeset -aU path

# Show all active key bindings.
bindkey

# Reassign keys.
bindkey "^[[3~" delete-char
bindkey "^[[F"  end-of-line
bindkey "^[[H"  beginning-of-line

# Make a variable value uppercase.
echo ${name:u}
echo ${(U)name}

# Make a variable value lowercase.
echo ${name:l}
echo ${(L)name}

# Declare a variable as inherently lower case or upper case.
# The variable will automatically be lower- or uppercased on expansion.
typeset -l name
typeset -u name

Alias expansion

When one writes an alias, one can also press ctrl-x followed by a to see the expansion of that alias.

Parameter expansion

Parameter expansions can involve flags like ${(@kv)aliases} and other operators such as ${PREFIX:-"/usr/local"}.
Nested parameters expand from the inside out.

If the parameter is a scalar (a number or string) then the value, if any, is substituted:

$ scalar='hello'
$ echo "$scalar"
hello

Curly braces are required if the expansion is followed by letters, digits or underscores that are not to be interpreted as part of name:

$ echo "${scalar}_world"
hello_world

If the parameter is an array, then the value of each element is substituted, one element per word:

$ typeset -a array=( 'hello' 'world' )
$ echo "${array[@]}"
hello world

The two forms array[@] and (@)array are equivalent:

$ echo "${(@)array}"
hello world

Parameter substitution

Check if a variable is set

Use the form +parameterName.
If name is set, even to an empty string, then its value is substituted by 1, otherwise by 0:

$ typeset name='tralala'
$ echo "${+name}"
1

$ name=''
$ echo "${+name}"
1

$ unset name
$ echo "${+name}"
0

Provide a default value

Use the forms parameterName-defaultValue or parameterName:-defaultValue.
If name is set then substitute its value, otherwise substitute word:

$ name='tralala'
$ echo "${name-word}"
tralala

$ name=''
$ echo "${name-word}"
(empty string)

$ unset name
$ echo "${name-word}"
word

In the second form:

  • only substitute its value if name is non-null, and
  • name may be omitted, in which case word is always substituted:
$ name='tralala'
$ echo "${name:-word}"
tralala

$ name=''
$ echo "${name:-word}"
word

$ unset name
$ echo "${name:-word}"
word

$ echo "${:-word}"
word

Just substitute with its value if set

Use the forms parameterName+defaultValue or parameterName:+defaultValue.
If name is set, then substitute it with its value, otherwise substitute nothing:

$ name='tralala'
$ echo "${name+word}"
word

$ name=''
$ echo "${name+word}"
word

$ unset name
$ echo "${name+word}"
(empty line)

In the second form, only substitute its value if name is set and non-null:

$ name='tralala'
$ echo "${name:+word}"
word

$ name=''
$ echo "${name:+word}"
(empty line)

$ unset name
$ echo "${name:+word}"
(empty line)

Set a default value and substitute

Use the forms parameterName=defaultValue, parameterName:=defaultValue or parameterName::=defaultValue.
In the first form, if name is unset then set it to word:

$ name='tralala'         # value: 'tralala'
$ echo "${name=word}"
tralala

$ name=''                # value: ''
$ echo "${name=word}"
(empty line)

$ unset name             # unset
$ echo "${name=word}"    # value: 'word'
word

$ echo "$name"
word

In the second form, if name is unset or null then set it to word:

$ name='tralala'         # value: 'tralala'
$ echo "${name:=word}"
tralala

$ name=''                # value: ''
$ echo "${name:=word}"   # value: 'word'
word

$ echo "$name"
word

$ unset name             # unset
$ echo "${name:=word}"   # value: 'word'
word

$ echo "$name"
word

In the third form, unconditionally set name to word:

$ name='tralala'         # value: 'tralala'
$ echo "${name::=word}"
word

$ echo "$name"
word

$ name=''                # value: ''
$ echo "${name::=word}"  # value: 'word'
word

$ echo "$name"
word

$ unset name             # unset
$ echo "${name::=word}"  # value: 'word'
word

$ echo "$name"
word

Fail on missing value

Use the forms parameterName?defaultValue or parameterName:?defaultValue.
In the first form, if name is set then substitute its value, otherwise print word and exit from the shell.

$ name='tralala'
$ echo "${name?word}"
tralala

$ name=''
$ echo "${name?word}"
(empty line)

$ unset name
$ echo "${name?word}"
zsh: name: word

In the second form, substitute its value only if name is both set and non-null:

$ name='tralala'
$ echo "${name:?word}"
tralala

$ name=''
$ echo "${name:?word}"
zsh: name: word

$ unset name
$ echo "${name:?word}"
zsh: name: word

Interactive shells return to the prompt.

If word is omitted, a standard message is printed in its place:

$ name=''
$ echo "${name:?}"
zsh: name: parameter not set

Matching and replacement

In the following expressions, when name is an array and the substitution is not quoted, or if the (@) flag or the name[@] syntax is used, matching and replacement is performed on each array element separately.

FIXME

Arrays

# Get a slice of an array.
# Negative numbers count backwards.
echo "${ARRAY[2,-1]}"

# Get all folders up to a non folder, backwards.
local COMMAND
local FOLDERS=()
for (( I = $# ; I >= 0 ; I-- ))
do
	if [[ -d "${@[$I]}" ]]
	then
		FOLDERS+="${@[$I]}"
	else
		COMMAND="${@[1,-$((${#FOLDERS}+1))]}"
		break
	fi
done

# Make entries unique in an array.
# See https://til.hashrocket.com/posts/7evpdebn7g-remove-duplicates-in-zsh-path.
typeset -aU path

Tests

# Regex match.
[[ "$OSTYPE" =~ "darwin" ]]
[[ "$OSTYPE" -regex-match "darwin" ]]
ls **/*(-@)

Key bindings

# Show all active key bindings.
bindkeys

# Make the home, end and delete key work as expected.
# To know the code of a key execute `cat`, press enter, the key, and Ctrl+C.
bindkey  "^[[H"   beginning-of-line
bindkey  "^[[F"   end-of-line
bindkey  "^[[3~"  delete-char

Configuration

Config files read order

  1. /etc/zshenv; this cannot be overridden
    subsequent behaviour is modified by the RCS and GLOBAL_RCS options:

    • RCS affects all startup files
    • GLOBAL_RCS only affects global startup files (those shown here with an path starting with a /)

    If one of the options is unset at any point, any subsequent startup file(s) of the corresponding type will not be read.
    It is also possible for a file in $ZDOTDIR to re-enable GLOBAL_RCS.
    Both RCS and GLOBAL_RCS are set by default

  2. $ZDOTDIR/.zshenv

  3. if the shell is a login shell:

    1. /etc/zprofile
    2. $ZDOTDIR/.zprofile
  4. if the shell is interactive:

    1. /etc/zshrc
    2. $ZDOTDIR/.zshrc
  5. if the shell is a login shell:

    1. /etc/zlogin
    2. $ZDOTDIR/.zlogin
  6. when a login shell exits:

    1. $ZDOTDIR/.zlogout
    2. /etc/zlogout

    This happens with either an explicit exit via the exit or logout commands, or an implicit exit by reading end-of-file from the terminal.
    However, if the shell terminates due to exec'ing another process, the files are not read. These are also affected by the RCS and GLOBAL_RCS options.
    The RCS option affects the saving of history files, i.e. if RCS is unset when the shell exits, no history file will be saved.

If ZDOTDIR is unset, HOME is used instead. Files listed above as being in /etc may be in another directory, depending on the installation.

/etc/zshenv is run for all instances of zsh.
it is a good idea to put code that does not need to be run for every single shell behind a test of the form if [[ -o rcs ]]; then ... so that it will not be executed when zsh is invoked with the -f option.

When /etc/zprofile is installed it will override PATH and possibly other variables that a user may set in ~/.zshenv. Custom PATH settings and similar overridden variables can be moved to ~/.zprofile or other user startup files that are sourced after the /etc/zprofile.
If PATH must be set in ~/.zshenv to affect things like non-login ssh shells, one method is to use a separate path-setting file that is conditionally sourced in ~/.zshenv and also sourced from ~/.zprofile.

History

# The maximum number of events stored in the internal history list.
# If you use the HIST_EXPIRE_DUPS_FIRST option, setting this value larger than
# the SAVEHIST size will give you the difference as a cushion for saving
# duplicated history events.
HISTSIZE=1000

# The file to save the history in when an interactive shell exits.
# If unset, the history is not saved.
HISTFILE=~/.histfile

# The maximum number of history events to save in the history file.
SAVEHIST=1000

Completion

# Enable completion.
autoload -U compinit
compinit

# Enable cache for the completions.
zstyle ':completion::complete:*' use-cache true

Prompt management

# Enable prompt management.
autoload -U promptinit
promptinit; prompt theme-name

Automatic source of files in a folder

# Configuration modules.
# All files in the configuration folder will be automatically loaded in
# numeric order. The last file setting a value overrides the previous ones.
# Links are only sourced if their reference exists.
: "${ZSH_MODULES_DIR:-$HOME/.zshrc.d}"
if [[ -d "$ZSH_MODULES_DIR" ]]
then
	for ZSH_MODULE in "$ZSH_MODULES_DIR"/*
	do
		[[ -r "$ZSH_MODULE" ]] && source "$ZSH_MODULE"
	done
	unset ZSH_MODULE
fi

Frameworks

Plugins

Awesome zsh plugins is a comprehensive list of various plugins for ZSH.

What follows are some I always add to my setup:

Troubleshooting

The delete, end and/or home keys are not working as intended

Some setting or plugin changed the key binding. Reassign them to obtain the expected behaviour:

bindkey  "^[[H"   beginning-of-line
bindkey  "^[[F"   end-of-line
bindkey  "^[[3~"  delete-char

To know the code of a key, execute cat, press enter, press the key, then Ctrl+C.

Compinit warnings of insecure directories and files

Compinit is complaining of some critical files being group writable. Running compaudit will list those files. Just use it to remove the group's write permission:

compaudit | xargs chmod g-w

Further readings