Zsh

Your new favorite programming language

Wednesday, Jun 17, 2020

Shell Scripting

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.

TIL: All of these shell scripting languages were written in the C programming language.

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

I/O

read

getopt

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
fi

# 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
fi

eval set -- "${options}"
while true; do
  case "$1" in
  -h|--help)
    print "I'm sorry Dave, I'm afraid I can't do that"
    ;;
  -o|--output)
    ofile=$2
    shift 2
    ;;
  -i|--input)
    ifile=$2
    shift 2
    ;;
  -w|--where)
    case "$2" in
      "")
        location="not specified"
        shift 2
        ;;
      *)
        location="$2"
        shift 2
        ;;
    esac
    ;;
  --)
    shift
    break
    ;;
  esac
done

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

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

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

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.

zsh

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
zsh

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

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

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 ;;
esac

/etc vs. ~/

You can actually tell which files were sourced by zsh on your command line by running the following command:

zsh -o SOURCE_TRACE

Command Substitution

For more information, see the COMMAND SUBSTITUTION section of man zshexpn

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)

Parameter Expansion

To learn more, see the PARAMETER EXPANSION section of man zshexpn

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.

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

Arithmetic Evaluation

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

Tip: You can use the ; character to signify a newline without actually providing one. This is useful for compressing a script or writing one-liners on your terminal.

# Check if the script is being executed by the root user
if [[ ${UID} -ne 0 ]]; then print "You are not the root user"; fi

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

Warning: In shell scripting, you can only use == and != to see if two strings are of equal value.

Arithmetic Evaluation

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.

ls

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
| Pipe
! 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.

/dev/null

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

read

print "Enter a number"
read num
print "You guessed $num"
# Save the result in the variable 'secret'
read -rs 'secret?Password:'
print "You entered ${secret}"
print "alpha bravo charlie" | read -A bases
print -l ${bases}
# alpha
# bravo
# charlie
read -u 0

Reading Files

files=/Users/austintraver/Downloads/*
for file in $files; do
  print $file
done

While loops

index=0
while [[ ${index} -lt 5 ]]; do
  print ${index}

  # Lame variable increment
  index=$((index+1))

  # L33t variable increment
  ((index+=1))
done

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

val='0123456789'
# [ Print the first three numbers ]
print ${val:0:3} #012
# [ Print every number after index 5 ]
print ${val:5} # 56789
# [ Print the last 3 numbers ]
print ${val:(-3)} #789
# [ Print everything except the first 2 numbers and last 3 numbers ]
print ${val:2:(-3)} # 23456
# [ Print two numbers starting from the 6th-to-last number ]
print ${val:(-6):2} # 45

Substring Matching

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

Look down at your keyboard

string='one/two/three/four/five'
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)oldsum##*}
# 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]}
spaces
var='Sentence one. Sentence two.'
print ${var[(w)4]}
two.
var='You can even get the word that comes last'
print ${var[(w)-1]}
last

Referencing Command History

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

Substituting Text in Previous Commands

# [ Option 2 ]
^brown^blue
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:

Parameter Expansion

Directory Expansion

command

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"
done
# 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"
done
# 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

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

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

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

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

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

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”

Globbing

Operator Expansion

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

Globbing

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

Section 14.8.7 of the manual covers all of this in greater detail.

Globbing Specific Filetypes

# All plain files
print ./*(.)

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

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

# [ Recursive Editions ]

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

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

Ownership Globbing

Owner

Group

World

Ascending Order

Checking if a Command Exists

Note: if there exists an alias by this name, it will return the alias definition instead of the path to the executable file. Furthermore, if the command is not found, it will print an error to the console.

Count the Number of Words in a String

Reading Words

Tip: zsh gives you a lot of flexibility with what syntax to separate arguments supplied to flags. You can use [...] <...> {...} or (...)

whence

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.

here-doc

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.

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

Ternary Operator

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

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

Note: Don’t do this with string comparisons because inside of (( … )) all strings are treated as the number 0

Warning: Be careful about comparing strings, because sorting is by lexicographical order which means that a word is sorted in the following way: treat each number as a word in a dictionary. the value of a word would correspond with its location in the dictionary. “a < z” because “a” would be at a lower index number in the dictionary

[[ "apple" < "banana" ]] && print "yes" || print "no"
# => "yes"

Warning: Inside of double brackets, the evalution is not a true ternary operator, because the third statement will still execute if an error is thrown by the second statement

[[ 1 -eq 1 ]] && asdf || print "Not true"
bash: asdf: command not found
Not true

Workaround: You can fix this problem by surrounding the truth evaluation by curly brackets, and appending a ;:; to the end of the statement. this will cause the command to report the error when it occurs, and then return true which will cause the or statement to not evaluate, since the first two statements returned true

[[ 1 == 1 ]] && { asdf ;:; } || print "Not true"
# => "bash: asdf: command not found"

ANSI C Quotations

print $'\u7231'
# => 爱
print $'\U0001f602'
# => 😂

Regular Expressions

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

pie=good
[[ $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'

Arithmetic Evaluation

a=2
b=4
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.

Note: this won’t work for floating points, because bash will truncate the decimals when evaluating division

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
}

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

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

shout
# => (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.

Good News: There’s a much less ugly way to do this in zsh version 5.0 using the ${array:|filter} syntax. It’s documented in “Parameter Expansion” of man zshexpn

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
esac

typeset

Printing Environment Variables

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:

print -P '%F{9}[red foreground]%f'
print -P '%K{blue}[blue background]%k'
print -P '%U[underline]%u'
print -P '%B[bold]%b'
print -P '%S[standout]%s

Printing %E will clear from the cursor to the end of the line.

Custom Keybindings

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

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):

compadd

If you want to have this:

hello <Tab>
# => cmd1    cmd2    cmd3

You’ll write this:

comdadd cmd1 cmd2 cmd3

_describe

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.

_arguments

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> ...
  1. 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
  identifier)
    #do some special work when we want completion for 2nd argument
    ;;
esac

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
        log)
            # This is for demonstration purpose only, you'll use _files utility to list a directories
            _describe 'command' "($(ls $1))"
            ;;
        cache)
            # This could be done above also, in _arguments, you know how :)
            compadd one two
            ;;
    esac
}

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.

zsh/nearcolor

Multios

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
setopt BAD_PATTERN

# 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
setopt BARE_GLOB_QUAL

# Match regular expressions in `=~` case sensitively
setopt CASE_MATCH

# Perform =file expansion
setopt EQUALS

# Perform history expansion with `!`
setopt BANG_HIST

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

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

# 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
setopt CHECK_JOBS
setopt CHECK_RUNNING_JOBS

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

# Treat '%' specially in prompt strings
setopt PROMPT_PERCENT

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

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
zprof
num  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)    1       9.37     9.37   19.02%      9.20     9.20   18.67%  _zsh_highlight_load
 4)    1       5.59     5.59   11.34%      5.59     5.59   11.34%  _zsh_highlight_bind
 5)    5       0.77     0.15    1.56%      0.77     0.15    1.56%  add-zsh-hook
 6)    1       0.45     0.45    0.90%      0.45     0.45    0.90%  bashcompinit
 7)    1       0.28     0.28    0.56%      0.28     0.28    0.56%  is-at-least
 8)    1       0.15     0.15    0.31%      0.15     0.15    0.31%  (anon)
 9)    1       0.09     0.09    0.19%      0.09     0.09    0.19%  compdef
10)    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

sudo chown ${UID}:${GID} $(compaudit)

Pretty-Printing Associative Array

Zsh Hashed Commands

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

hash

enable

disable

unhash

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

Terminal

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

Signals

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

Tip: Many people don’t know that on Mac OS, there’s an alternative to using ⌃ C to end a program: ⌘ .

Signals

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.

# [Kill a processes by PID]
kill -9 <process_id>
# [Kill a process by name]
pkill "java"
# [Kill a process running on a specific port]
kill $(lsof -t -i :4000)
# [Send the SIGTERM (15) to process 123]
kill -15 123
# [Send the SIGTERM (15) signal to process 123 & process 456]
kill -TERM 123 456
# [Send the SIGINT (2) signal to process 123]
kill -2 123
# [Send the SIGSTOP () signal to process 123]
kill -TSTP 123
# [Send the SIGINT (2) signal to job ID # 1]
kill -2 %1

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

Warning: The snapshot that you’re given with $ df -kh is not always recent. If you’ve made some big changes to the available amount of storage, the discrepancy could be very large. To get an up-to-date version of the available storage on your computer, type $ du -chs

Customization

Custom Bash Prompt

The bash prompt is actually a collection of several prompts.

Directory Structure

$PATH

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

echo -e ${PATH//:/\\n}

Normally each directory in the path is seperated by a : not a newline, but I find this to be a clearer output.

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!

greet

#!/usr/local/bin/python3
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]
./greet

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

/usr/local/bin

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}

/usr/local/sbin

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

# [Open the Postman.app file explicitly]
open ~/Applications/Postman
# [Open the application "Postman"]
open -a Postman
# [Open a website in Safari]
open -a Safari 'https://google.com'
# [Open with the default text editor]
open -t textfile.txt
# Launch a new instance of the application
open -n sample.png

on Linux

Opening an application on Linux is as easy as typing

# [Launch any application located in $PATH]
appname

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.

caffeinate

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