Shell Scripting Primer

There are a handful of shell scripting languages, many of which will come by default with your operating system. On both macOS and Linux based operating systems, you can count on having at least one of the following shells by default.

This guide uses zsh as the shell language of choice, so your mileage may vary if you try to use these commands in another shell scripting language. The reason zsh is chosen is because it's the default shell on the macOS operating system, but more importantly, because it is my favorite shell

Key Concepts

This guide is more of a reference than a tutorial, so expect the sections to jump around a fair bit.

People often forget the distinction between argument, option, and parameter




There exists a builtin command getopts but it does not accept long-form command arguments. Therefore, it is useful to use GNU's getopt utility function to parse command-line arguments in shell scripts.

Useful Flags:

For each option declared after the --options or --long flag, that option can be proceeded by 1 colon, indicating that this option has a required argument, or by 2 colons, indicating that this option has an optional argument

It's a little easier to explain with an example:

if [[ $# -eq 0 ]]; then
  print "Error: no options provided" >&2
  exit 1

# Call getopt to validate the provided input.
options=$(getopt -o ho:i:w:: -l help,output:,input:,where:: -- "$@")

if [[ $? -ne 0 ]]; then
  print "Error: incorrect options provided" >&2
  exit 1

eval set -- "${options}"
while true; do
  case "$1" in
    print "I'm sorry Dave, I'm afraid I can't do that"
    shift 2
    shift 2
    case "$2" in
        location="not specified"
        shift 2
        shift 2

if [[ ${ifile} ]]; then
  print "Input file is ${ifile}"

if [[ ${ofile} ]]; then
  print "Output file is ${ofile}"

if [[ ${location} ]]; then
  print "Location is ${location}"

Tilde Expansion & Wildcards

Login Shells

A login shell is started when you open your terminal, or login to another computer via ssh. But here is where it gets tricky. If you open your terminal, and you see your shell prompt, opening up a new shell inside of it would not be a login shell.


And yet, the following would be a login shell, because it uses the -l flag to log in.

zsh -l

Run each of these commands below to help you test whether or not your shell is a login shell:

# Run this command first
if [[ -o login ]]; then; print yes; else; print no; fi

# Enter a non-login shell

# Run this command second
if [[ -o login ]]; then; print yes; else; print no; fi

# Exit the non-login shell that you just opened up

A script is non-interactive, since it's executed as a command, and freezes your terminal until you finish. However, a script will be treated as interactive if it is executed with the -i flag. You can test this with the code below.

A shell is interactive as long as it was not started with either a non-option argument or the -c flag.

case "$-" in
  *i*)    print This shell is interactive ;;
  *)    print This shell is not interactive ;;

Subshells will retain the value of variables exported to the environment. In order to create a subshell with a clean environment, you need to pass specific commands to exec and zsh, as shown below:

User and System runtime configurations

It comes down to whether the files are in /etc or ${HOME}.

Command Substitution

Sometimes you're in a situation where you'd like to run a command, but you don't know what the input value should be yet. Yes, you could save a variable, but that wouldn't be the properly lazy way of doing things. You can treat a substitute the output of a function by placing it inside $(here)

Using command substitution allows us to take the output from a command, and use at as the input for a different command. In the following example, the output of the command whoami is substituted as input for the command print:

print "My name is $(whoami)"

My name is ttrojan

Parameter Expansion

Expansion Modifiers

Conditional Expressions

When coding, we typically expect less than to be represented by the < character. In shell scripting, however, the < symbol has an entirely seperate meaning (more on that later). To perform an equality check, we have to use -lt to signify the same meaning. Also, we will use square brackets [[ ]] to contain the statement, and literally specify then as well as the end of our if statement. An example is provided below.

if [[ ${name} == 'Austin' ]]; then
  print "His name is Austin"
  print "his name is not Austin"

All Conditional Flags

Comparator Meaning
-eq is equal to
-ne is not equal to
-lt is less than
-le is less than or equal to
-gt is greater than
-ge is greater than or equal to
-z is null

Arithmetic Evaluation

if (( number < 5 )); then
  print "Number is less than five"
  print "Number is not less than five"


Number is less than five

In order to perform arithmetic operations, surround variable names, integers, and operators in a ((...)) double quotations, like this:

If you don't do that, the variable is interpreted as a string, and the number will be appended to the variable's current value.


Useful Flags

Flag Purpose
-A Reveal hidden files (except . and ..)
-l List in long format
-h Describe file-size in terms of G, M, K, etc.
-k Describe file-size in terms of kilobytes
-t Sort by time modified (newest -> oldest)
-u Sort by time last accessed (newest -> oldest)
-S Sort by size (largest -> smallest)
-r List the files in reverse lexicographical order

Colorized ls

Escaping Characters

Some characters take on a special meaning when they are escaped

Character Escaped Meaning
\a alert (bell)
\b backspace
\e escape
\n newline
\r carriage return
\t tab
\x41 1-byte UTF-8 character
\u2318 2-byte UTF-8 character
\U0001f602 4-byte UTF-8 character

Some characters are special by default, and must be escaped by the escape character \ in order for zsh to interpret them literally

Character Un-Escaped Meaning
\ Escape character
/ Pathname directory separator
$ Variable expression
& Background job
? Character wildcard
* String wildcard
( Start of sub-shell
) End of sub-shell
[ Start character-set wildcard
] End character-set wildcard
{ Start command block
} End command block
` `
! Logical NOT
; Command separator
' Strong quote
" Weak quote
~ Home directory
` Backtick
# Comment

Some characters are special, but only some of the time, such as ,, for example, in the case of brace expansion

print cod{e,er,ing}

code coder coding

Single Quotes

If a string is inside of single quotes, every character inside will be preserved literally, thus no escaping is possible. The only character that can't be typed is the single quote ' which signals the end of the string.

Double Quotes

Double quotes are a little more permissive than single quotes.

Run a job in the background

When a program is run in the background, the program is forked, and run in a sub-shell as a job, running asynchronously. You can add a single ampersand & at the end of a command to have it run in the background.

Run commands sequentially

You can use a double ampersand && to signal for commands to run in sequential order. The second command will only run if the first command doesn't fail. A program fails if it returns a number other than 0.

You can use a double pipe || to signal for a command to run only if the previous command fails.


Your own personal black hole

/dev/null is a very useful tool. If you're ever in a situation where you need to redirect output, but you have nowhere to put it, you can redirect it to /dev/null. This will accept input and essentially vaporize it. You won't see anything printed to your terminal, and nothing will be saved to a file.

Run this command in your terminal to see what happens.

print "Silenced" &> /dev/null

However, there's an even easier way to do it. You can combine stdout and stderr, file descriptors 1 and 2 respectively using the &> redirection command, and then append a - to close both of the file descriptors.

Easier to demonstrate with an example

func() {
  print "Standard Output" >&1
  print "Standard Error" >&2

Shell Arguments, Options, Flags

Sometimes you want to specify an option/flag (same thing)

Maybe -v should print verbose output. But other times, there's an argument associated with the flag, such as -o file.txt. In this case, file.txt is known as an option argument

Parsing Command-Line Arguments

The keyword ${@} contains the set of all arguments to a program/function

Reading I/O



While loops

typeset -i index=0
while (( ${index} < 5 )); do
  print ${index}

  # Lame variable increment

  # L33t variable increment


Anonymous Functions

Zsh supports anonymous functions, which allow us to prevent variables from leaking in scope.

This is particularly useful when you're building a script that will be sourced by the shell, such as .zshenv or .zshrc. Ideally, we'd prevent our script from polluting the shell environment with variables that are left lingering at the end of our script's execution. We can do so by nesting our code inside of anonymous functions.

Without using an anonymous function, the identifier used as the iterator in a for-loop persists beyond the evaluation of the for-loop itself:

integer i
for i in {1..3}; do 
    print ${i}; 
integer -p i


typeset -i a=3

By nesting our declaration of the for-loop iterator within an anonymous function, we can prevent the scope of the variable from leaking into the greater namespace

    integer i
    for i in {1..3}; 
        do print ${i}; 
integer -p i


integer: no such variable: i

You can also use the pre-increment (++i) and post-increment (i++) operators within the double parenthesis block (( ))

String Manipulation

String manipulation allows you to rename files, implement boolean logic, along with many other uses. Every variable stored as string can be referenced with the syntax ${string:position:length}

Index Slicing

Substring Matching

If you're looking for a way to remember these, there's a trick I use:

Look down at your keyboard

print ${string#*/} # two/three/four/five
print ${string##*/} # five
print ${string%/*} # one/two/three/four
print ${string%%/*} # one

Length of a String

checksum=${(s< >)$(shasum -a 256 file.txt)[1]}
print ${(N)checksum##*}
# 64

Cutting Out The Middle

Using the parameter expansion flag (S), you can actually specify for the pattern to match substrings, similar to the way grep and sed work. For the # and % parameter expansion flags, they will still seek to cut from the beginning and the end respectively, but will cut out the first match found to the pattern (non-greedy) from the middle of the string. You can use ## and %% to perform greedy searches.

Splitting Strings

You can index a string by its word index (1-indexed), even if there is punctuation in the sentence by using the (w) flag inside of square braces.

var='This sentence   has  inconsistent spaces'
print ${var[(w)5]}


var='Sentence one. Sentence two.'
print ${var[(w)4]}


var='You can even get the word that comes last'
print ${var[(w)-1]}


Referencing Command History

Next, attached below are expansions for arguments outside the context of command history

Substituting Text in Previous Commands

# [ Option 2 ]

print the quick blue fox

Global Substitution:

Using the previous syntax, you will only replace the first instance matched. If you want to replace all matches to the pattern, use the syntax below:

Directory Expansion

Loading Bar

You can use ANSI escape codes to make a loading bar

for i in {1..100}; do
  # Print the loading as a percentage, with color formatting
  print "Loading: \x1b[38;5;${i}m${i}%%\x1b[0m\n"
  sleep 0.01
  # Return to the previous line above
  print "\x1b[1F"
# Return to the line below
print "\x1b[E"
for i in {1..255}; do
  print "\x1b[38;5;${i}mwow\x1b[0m\n"
  sleep 0.01
  print "\x1b[1F"
# Return to the line below
print "\x1b[E"

Read Words Into Array

Sending Signals With Kill

The builtin command kill is used to send signals to a process.

You can specify the signal by its number, or by its name.

Handling Signals With Trap

    print "TRAPINT() called: ^C was pressed"

    print "TRAPQUIT() called: ^\\ was pressed"

    print "TRAPTERM() called: \`kill\` command received"

    print "TRAPEXIT() called: happens at the end of the script no matter what"

for i in {1..5}; do
    print ${i}
    sleep 1

For all of these TRAP[NAL]() functions, if the final command is return 0 (or if there is no return statement at all, then code execution will continue where the program left off, as if the signal was accepted, caught, and handled. If the return status of this function is non-zero, then the return status of the trap is retained, and the command execution that was previously taking place will be interrupted. You can do return $((128+$1)) to return the same status as if the signal had not been trapped

fc "find command"


Note that this is not sorted numerically. However, it is possible to specify this. To do so, specify the glob qualifier n in your filename generation pattern, such as in the example below.

Operator Expansion

If name is an associative array, the expression ${(k)name} will expand to the list of keys contained by the array name


zsh is capapable of some very powerful globbing. Without setting any options, you can recursively iterate through directories with **/*.

Glob Options

Included below are some features of the extended glob option:

Glob Qualifiers

This topic is covered in greater detail on the zsh.fyi article about expansion

Here are some flags below:

Globbing Specific Filetypes

Below are some qualifiers related to the type of file

Glob Qualifier Meaning
/ directories
. plain Files
= sockets
* executable files
% device files
@ symbolic Links

Below are some qualifiers related to the access permissions of the file

Owner Group World
Readable r A R
Writable w I W
Executable x E X

Additionally, I've included some extra examples below

Glob Qualifier Meaning
F Full directories
^F Empty directories and all non-directories
/^F Only empty directories
s setuid files 04000
S setgid files 02000
t sticky bit files 01000
# All plain files
print ./*(.)

# Anything but directories
print ./*(^/)

# Only empty directories
print ./*(/^F)

# [ Recursive Editions ]

# All plain files
print ./**/*(.)

# Anything but directories
print ./**/*(^/)

You can use o in conjunction with some other keywords to sort the results in ascending order

Checking if a Command Exists

Using equals expansion

A simple way to perform a check is by using equals expansion (e.g. ), which will search the directories in path for an executable file named FILENAME

if [[ =brew ]]; then
    print "Command is an executable file"
    print "Command not found"

Using parameter expansion

# [ Right way, note the (( parentheses )) ]
if (( ${+commands[brew]} )); then
    print "Command exists"
    print "Command not found"

Count the Number of Words in a String

Reading Words

Below is an example of how to print a list of all words present in a file words.txt removed of any duplicates

the day is sunny the the
the sunny is is


The whence command is very useful, and can replace many common commands

Finding Commands Matching a Pattern

You can use the -m option to match a pattern instead of a command name. Be sure to use a quoted string for your pattern, otherwise it is subject to filename expansion.


Sometimes you want to supply some text in a script across multiple lines. Furthermore, this is happening at a point where you're already in some nested layers of indented logic. Luckily zsh provides a way to supply a multi-line string, stripped of any leading \t tab characters. It's called a here-doc and it's referred to with the <<- operator.


A here-string is documented exactly twice by name in the entire zsh manual. Writing down how it works here, so that I know for next time...

Expanding Parameters in Files

If you have a super long string of text, for instance, a SQL query, you may want to save the contents of that query in a different file. It's possible you may need to store a variable in the query, and if so, you can use the (e) paramater expansion flag when referencing the string. This flag causes the string to have any ${variable} words treated as if they were a normal shell variable, and not text from the file.

exit vs. logout

Brace Expansion

Brace expansion can be used in powerful ways, namely to be lazy, the most powerful force in the universe.

The ternary operator

Ternary operators are supported in Zsh, but only when they are used within an arithmetic evaluation, such as (( a > b ? yes : no ))

max=$(( a > b ? a : b ))
print "The max is ${max}"

The max is 6

[[ "apple" < "banana" ]] && print "yes" || print "no"
# => "yes"
[[ 1 -eq 1 ]] && asdf || print "Not true"

bash: asdf: command not found Not true

[[ 1 == 1 ]] && { asdf ;:; } || print "Not true"

"bash: asdf: command not found"

ANSI C Quotations

Regular Expressions

The zsh/regex module handles regular expressions. There's support for PCRE regular expressions, but by default regular expressions are assumed to be in Extended POSIX form.

You can use the =~ operator to test a value against a pattern

[[ $pie =~ d ]] && print 'Match found'
[[ $pie =~ [aeiou]d ]] && print 'Match found'
# No match because the regular expression has to capture the value of
# the variable, not the variable itself
[[ $pie =~ [p][i]e ]] || print 'No match found'
# No match because there's no literal '[aeoiu]d' inside the word "good"
[[ $pie =~ "[aeiou]d" ]] || print 'No match found'

The value of the match is saved to the MATCH variable. If you used capture

On successful match, matched portion of the string will normally be placed in the MATCH variable. If there are any capturing parentheses within the regex, then the match array variable will contain those. If the match is not successful, then the variables will not be altered.

if [[ 'My phone number is 123-456-7890' =~ '([0-9]{3})-([0-9]{3})-([0-9]{4})' ]] {
    typeset -p1 MATCH match

typeset MATCH=123-456-7890 typeset -a match=( 123 456 7890 )

Arithmetic Evaluation

print $((a*b)) # => 8

# You can even do assignments.  The last value calculated will be the output.
b=$(( a *= 2 ))
print "b=$b a=$a"
# b=4 a=4

Floating Point Arithmetic

a=$(( 1 + 1 ))
message="I don't want to brag, but I have like $(( a + 1 )) friends."
print $message

I don't want to brag, but I have like 3 friends.

print "6 / 8 = $(( 6 / 8 ))"

6 / 8 = 0

print "6 / 8 = $(( 6 / 8.0 ))"

6 / 8 = 0.75

File Descriptors

Custom File Descriptor

You can create your own file descriptor number and have it direct to any file you'd like.

exec 3> ~/three.txt

print 'one' >&1
print 'two' >&2
print 'three' >&3

exec 3>&-
exec {four}>&-
# Open file descriptor 3, Direct output to this file descriptor
# toward the file ~/three.txt
exec 3> ~/three.txt
# Open file descriptor allocated by shell to unused
# file descriptor >= 10. Direct output to this file descriptor
# toward the file ~/fd.txt
exec {fd}> ~/fd.txt
# (alternative: sysopen -w -u 3 /dev/null)

shout() {
  print 'File descriptor 1' >&1
  print 'File descriptor 2' >&2
  print 'File descriptor 3' >&3
  print 'File descriptor fd' >&$fd

# => (1:) 'File descriptor 1'
# => (2:) 'File descriptor 2'

# Close file descriptor 3
exec 3>&-
# Close file descriptor fd
exec {fd}>&-

# => (1:) 'File descriptor 1'
# => (2:) 'File descriptor 2'
# => (3:) 'error: bad file descriptor'
# => (12:) 'error: bad file descriptor'

Technically this is a little dangerous, especially for file descriptors 3-8, (for instance, #5 is used when spawning child processes), so it's best to do the alternative "variable name" method, shown below

# Open file descriptor `fd` that redirects to 'output.txt'
exec {fd}> ~/output.txt

print "{fd} points to file descriptor ${fd}"
# => "{fd} points to file descriptor 12"

print $'alpha\nbravo\ncharlie' >&$fd

# Close file descriptor 'fd'
exec {fd}>&
print $'alpha\nbravo\ncharlie' >&$fd

Disowning a Job

If you have a command that you'd like to continue running, even after the shell has been closed, you can use the disown builtin command. There is an explicit syntax, and a short-hand syntax.

Sometimes symbolic links point to files that don't exist, it's useful to delete them, and zsh makes that super simple by using glob qualifiers.

Remove Element From Array

Sometimes you have an array of elements, and you need to remove a value from the array, but you don't know the index that this value is located at.

You can also remove elements from an array based on patterns. This filter takes on the syntax ${array:#PATTERN} where PATTERN is the same as the form used in filename generation.

Background Jobs

Checking For a Command

The commands variable in zsh is an associative array whose keys are all of the commands that can be used, and whose values are the corresponding filepaths to where those commands are located. The + operator when applied to an associative array will have the variable expand to 1 if the key is found, and 0 if the key is not found.

Parsing Command Options

The zparseopts module can be used to create a function or program that can accept command-line options. For more information about how to use it, you can search for zparseopts in man 1 zshmodules

Attached below you will see a wrapper I wrote for the transmission command line interface, as there is no way to cleanly alias the transmission command without writing a wrapper like this, as it installs as five separate commands.

# Parse the following command-line arguments as options

# Note on `-a`:
# Specify the name of where to save parsed options
# In this case, into an array named `option`

# Note on `-D`:
# if an option is detected, remove it from
# the positional parameters of the calling shell

zparseopts -D -a option \
    'c' '-create' \
    'd' '-daemon' \
    'e' '-edit' \
    'r' '-remote' \
    'q' '-quit'
case ${option[1]} in
    -c | --create)
    transmission-create ${@}
    -d | --daemon)
    transmission-daemon ${@}
    -e | --edit)
    transmission-edit ${@}
    -r | --remote)
    transmission-remote ${@}
    -q | --quit)
    transmission-remote --exit
    exit 0


The typeset builtin declares the type of a variable identified by a name that is optionally assigned a value value. When an assignment is not made, the value of name is printed as follows:

typeset -i a=1

typeset a


typeset b=1
typeset b=1

Flags to state variable type

Flags to state variable properties

Flags to modify command output

Matching a Certain Type

Matching a Certain Pattern

typeset +m 'foo*'

foo foo_fighters food

typeset -m 'foo*'

foo=bar foo_fighters=awesome food=(my life)

typeset -p -m 'foo*'

typeset foo=bar typeset foo_fighters=awesome typeset -a food=( my life )

Pairing Scalars and Arrays

If you're using a shell scripting language, you often have to export directories to the environment, such as for PATH, which requires a list of directories separated by a colon.

Zsh gives you the ability to link two variables together, a scalar and an array. You can specify the delimiter that separates elements, and once you have, adding items to the array will add items to the scalar. An example is provided below:

Printing Colors

Printing colors can be done with SGR escape codes, explained on the ASCII page, but you can also do it with the prompt string format specifier syntax outlined below:

For each of the following examples, we'll format the scalar text

text="Hello world"

Additionally, you can use %S for standout formatting, which swaps the foreground and background colors.

Custom Keybindings

Use the zle module for binding custom keys, code written using zle can be sourced in your configuration files.


Useful Oreilly Resource


Completion Functions

Let's say our program is called hello.

Here's what will happen:

  1. You write a completion function, typically _<cmd-name>
_hello() {
  # You write your code here
  1. You bind your function to a command
compdef _hello hello

Whenever you want to throw out possible completions, you'll use one of the following utility functions(in this post):


If you want to have this:

hello <Tab>
# => cmd1    cmd2    cmd3

You'll write this:

comdadd cmd1 cmd2 cmd3


If you want to have this:

hello <Tab>
# => cmd1    --  description1
# => cmd2    --  description2

You'll write this:

_describe 'command' "('cmd1:description1' 'cmd2:description2')"

Note: In both of above commands, we didn't consider which argument no. it is, means even hello cmd1 <Tab> will give same output. Next command will solve this problem.


Now this is a powerful one. You can control multiple arguments.

By multiple arguments I mean hello arg1 arg2 not hello arg1|arg2

Here's the basic syntax: _arguments <something> <something> ... where <something> can either be:

First one is self-explanatory, whenever called it'll output the description:

hello <Tab>
-o  --  description

For the second one, <argument number> is self-explanatory. I'll leave message empty to demonstrate a minimal example. For <what to do>, it can be quite a few things, two of which are provided below:

  1. List of arguments possible at given argument number. For example, if two arguments(world and universe) are possible at argument one(hello world|universe), we can write:

    _arguments '1: :(world universe)' <something> ...
  2. Set variable state to an identifier. For example, if we want to call another function at argument no. 2, we can write:

    typeset state
    _arguments '2: :->identifier'
    case ${state} in
        #do some special work when we want completion for 2nd argument

That might be confusing, lets sum up _arguments by an example:

Lets say, our program has possible args like:

hello [cat|head] <file at /var/log> one|two

Its completion function can be:

_hello() {
    local state
    _arguments '1: :(cat head)' '2: :->log' '3: :->cache'
    case ${state} in
            # This is for demonstration purpose only, you'll use _files utility to list a directories
            _describe 'command' "($(ls $1))"
            # This could be done above also, in _arguments, you know how :)
            compadd one two

Job Control

There are several ways to refer to jobs in the shell. A job can be referred to by the process ID of any process of the job or by one of the following:

Zsh Modules

Zsh comes with many useful modules, but none are loaded by default. This is done in order to prevent optional features from needlessly slowing down the shell's startup time.



date >&1 >file

Operating System Commands

There are some ANSI escape sequences that allow you to write Operating System Commands (OSCs)

Default Zsh Options

Included below, more for my reference, but could be helpful for anyone

# Print an error if a glob pattern is badly formed

# Print an error if a glob pattern does not match any files
setopt NOMATCH

# Treat unset parameters as '' in subs, 0 in math, otherwise error
setopt UNSET

# Consider parentheses trailing a glob as qualifiers

# Match regular expressions in `=~` case sensitively

# Perform =file expansion
setopt EQUALS

# Perform history expansion with `!`
setopt BANG_HIST

# Calling `typeset -x` implicitly calls `typeset -g -x`

# Allows a short-form syntax for `if`, `while`, `for`, etc.

# Run all background jobs at a lower priority
setopt BG_NICE

# Report the status of background jobs (typically it isn't done until <CR>)
setopt NOTIFY

# Confirm before logoff w/ background/suspended jobs

# Send the HUP signal to running jobs when the shell exits
setopt HUP

# Treat '%' specially in prompt strings

# Set $0 equal to name of script for funcs & script

Short Form Syntax

Zsh supports the traditional syntax for conditional statements and for loops. However, they also provide some more modern versions of each, as demonstrated below:

Silent Functions

You can specify that a function can be silent in its declaration! If you know you're going to make a helper function that you don't want to ever see output from, you can define it using the syntax outlined in the example below:

Zsh Time Profiling

zmodload zsh/zprof
# Start up functions in ~/.zshrc

calls time self name

  1. 2 22.18 11.09 45.03% 22.18 11.09 45.03% compaudit
  2. 1 32.66 32.66 66.29% 10.48 10.48 21.27% compinit
  3. 5 0.77 0.15 1.56% 0.77 0.15 1.56% add-zsh-hook
  4. 1 0.45 0.45 0.90% 0.45 0.45 0.90% bashcompinit
  5. 1 0.28 0.28 0.56% 0.28 0.28 0.56% is-at-least
  6. 1 0.15 0.15 0.31% 0.15 0.15 0.31% (anon)
  7. 1 0.09 0.09 0.19% 0.09 0.09 0.19% compdef
  8. 1 0.18 0.18 0.37% 0.09 0.09 0.18% complete

Zsh Completion Audit

To fix any ownership problems experienced during zsh completion, you can run the script below

for line in $(compaudit &>1); do
    if [[ -e ${line} ]]; then
        sudo chown ${UID}:${GID} ${line}
        sudo chmod -v 755 ${line}

Pretty-Printing Associative Array

Zsh Hashed Commands

Instead of searching the path each time for a command, Zsh hashes commands





You can use the unhash tool to remove almost any type of command from your current shell.


Below are some messy notes from a previous page I had dedicated to terminals, which, for the time being, is being placed here as a dedicated terminal page is difficult to expand upon when there's also a dedicated shell scripting page.

Common Movement Shortcuts

Shortcut Output
⌃ A Go to the beginning of the line
⌃ E Go to the end of the line
⌥ F Move forward one word
⌥ B Move back one word

Clearing Text

Shortcut Output
⌘ K Erase the entire terminal
⌘ L Erase the last command's terminal output

Modifying Chars

Shortcut Output
⌃ F Move forward 1 char
⌃ B Move backward 1 char
⌃ H Delete char left of cursor
⌃ D Delete char right of cursor
⌃ T Swap the last two chars

Modifying Words

Shortcut Output
⌥ L lowercase word right of cursor
⌥ U uppercase word right of cursor
⌥ C title-case word of cursor
⌃ Y Paste the word that was deleted
⌥ T Push the word left of the cursor forward by one word

Modifying Lines

Shortcut Output
⌃ K Erase line right of cursor
⌃ U Erase line left of cursor
⌃ W Erase argument left of cursor
⌃ Y Paste what was just erased
⌃ A Go to the beginning of the line
⌃ E Go to the end of the line

Undo Action

Shortcut Output
⌃ - Undo last keystroke

Command Selection

Shortcut Output
⌃ P Select previous command
⌃ N Select next command
⌃ R (1) Recall a previous command
⌃ R (2) Recall the next match
⌃ G Exit from command recall mode
⌥ R Restore altered command back to it's original state
⌃ J Submit command

Completion Shortcuts

There are a bunch of shortcuts that will help you complete the filename, or the command name, etc., but let's be real here. You're just going to keep using tab anyway. Save your energy for learning some of the other great shortcuts on here.

Misc Input

Many of the keys you normally press can be entered with a control key combo instead.

Shortcut Output
⌃ I tab
⌃ J newline
⌃ M enter
⌃ [ escape
⌃ D $ exit closes the entire terminal session
Shortcut Output
⌃ < Go to beginning of history
⌃ > Go to end of history


Ranked from weakest to strongest

Shortcut Output Signal Number Notes
⌃ Z (1) Pause a job SIGTSTP 20 Also known as suspending a job
⌃ Z (2) Continue a job SIGCONT 18 Pressing ⌃Z again will continue a process that was just suspended
^ C Interrupt a job SIGINT 2 Tell a process that it should not continue, the most common way to end a program
⌃ \ Quit a job SIGQUIT 3 Similar to an interrupt, but a little stronger (can still be caught), and will produce a core dump. The strongest of the signals that can be called via keyboard shortcuts


Various signals can be sent in UNIX to interact with a program. Many of these contain keyboard shortcuts, but first it is important to go over the most common types of signals. Programs can customize how they react to signals by catching, handling, or ignoring them.

To view all signals, type $ trap -l

To view all signal keyboard shortcuts, type $ stty -e or $ stty all

Signal Definitions

The Foreground & Background

The jobs Program

The jobs program lets you see information about the current jobs running from this terminal.

If you begin running a process, but it looks like it will take a long time to run, there's no need to open a new terminal tab. Instead, you can run the current process in the background. First, suspend (pause) the job with ⌃Z

If you have suspended multiple jobs, you can bring a specific job back to the foreground/background as follows

The ps Program

The pgrep Program

To find out the process ID of a particular program, use the pgrep program.

Managing active processes

Every process has a process ID or "PID" and there are a variety of commands that you can use to manage your active processes.

The kill Program

Using the kill program, you can send any active process a signal.

The pkill Program

Similar to kill except instead of killing processes by id, it kills processes by name.

# [Send the SIGTERM signal to all programs matching "java"]
pkill -15 java
# [Send the SIGTSTP signal to all programs named exactly "java"]
pkill -TSTP -x java

Managing Disk Space

The df Program

The df program, can be used to "display free" storage available on the computer.

# Get a report of the last recorded amount of memory
$ df -kh
# Refresh this value
$ du -chs

Useful df flags


Custom Bash Prompt

The bash prompt is actually a collection of several prompts.

Directory Structure


When you type the name of a function on the command line, it usually requires that you tell it the language and the directory. (e.g. $ python3 greet.py)

However, if the executable file is located in one of the directories specified by your $PATH, then it will automatically find and run the program without you needing to provide those specifications. It searches every directory specified in your PATH and runs the first file it finds with a matching name.

Seeing which directories are in your $PATH

# This one only works on zsh
print -l ${path}

# This one works on bash as well
echo -e ${PATH//:/\\n}

Using #! the "hashbang"

Sometimes you open up a file and it contains the first line, or something similar, to the one I've written below in a program called greet that prints Hello world!


print("hello world")

That first line uses a hashbang. What it does, is it tells your computer what program to use when trying to run the code specified in the lines below. In this case, it says to use the python3 program located in the directory /usr/local/bin

Assuming this was a file in your present working directory with executable permissions (if it isn't, type $ chmod +x greet in your terminal) then you could type $ ./greet and this file would run fine. You didn't need to specify that it needed to run with $ python3 greet

# [Hard way]
/usr/local/bin/python3 greet
# [Medium way]
python3 greet
# [Easy way]

Typical $PATH directories

The root directories

Note that / itself is the root directory, these are directories inside the root directory

The /bin directories

These are programs that are needed if you start the system with single user mode. Single user mode is a startup mode even more barebones than recovery mode.

The /local directories


This is for programs that are local to your user account. If you install a program here (and you should), then the other accounts on the computer won't be able to use it. Also, it's automatically in your ${path}


This is the local system bin, which is used for programs that are needed to boot the system, but that you won't be executing directly.

The command path

If you want to add a directory to ${path} you'll need to edit your ~/.zshrc. To add the directory /Users/tommytrojan/programs to your path, you would add the following line.

This will append /Users/tommytrojan/programs to the existing value of ${path} which is accessed by typing ${path}.

The export keyword

We used the export keyword when we updated the $PATH in our .zshrc but it's important to understand what it does. The export keyword will save a variable in the shell, but it will also save the variable in any sub-shells. That means if you called a function from your terminal, and that function checked for a variable $(PATH) it would still "remember" what that variable's value was set to be.

The Root User

On UNIX systems, the root user has capabilities that are disabled when you are logged in as a regular user. Type the command below to run a shell as the root user

sudo -i

From here, you can type any command without having to use the sudo command.

Opening applications

on MacOS

But there are very useful flags you can use, to type these out in the future

on Linux

Opening an application on Linux is as easy as typing

# [Launch any application located in $PATH]

The Welcome Message

Silencing the Welcome Message

Usually when you open your mac, you'll see a message such as

"Last login: Fri May 3 21:14:20 on ttys000"

But you can disable this message by adding a .hushlogin file to your home directory.

# [Silence the login message]
touch ~/.hushlogin

Alternatively, you can customize the message by modifying the contents of the file located in /etc/motd

Hidden Programs

On Mac OS, there are some really cool hidden programs that most people don't know about.


Many people don't know about caffeinate, a program you can use to prevent your computer from falling asleep.

Wake up a sleeping remote computer with ssh

# For a moment
ssh tommy@remote.net 'caffeinate -u -t 1'

Useful Flags

Add this line to your .inputrc so that when you type cd and try to tab-complete to a symbolic link to a directory, it will include the trailing / at the end.

set mark-symlinked-directories on

Advanced Tab Completion

If you are typing out a command, and you include environment variables (e.g. $PATH) or an event designator (e.g. !!) then you can press after typing it, and the terminal will immediately replace that reference with the actual argument that it evaluates to.

echo $HOME<TAB>
echo /Users/austin

Speak from Terminal

# Speaking from terminal
say 'hello world'

# Singing from terminal
say -v 'good news' di di di di di di

Arithmetic Expansion

Boolean Shell Builtins

This is an example where the shell will print success if the commands whoami and hostname both return status code 0.

if whoami && hostname; then
    print 'success'

ttrojan challenger success

You don't have to use real commands, you could use the shell builtin true, which always returns status code 0. (In fact, it's all that true actually does!)

if true && true; then
    print 'success'


Operator Precedence

Proof that || has operator precedence over &&


The zmv command is an alternative to mv, and can be loaded into the shell environment using the autoload command.