Zsh
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.
sh: the Bourne shell, written in 1977 for Unixksh: the Korn shell, written in 1983 by David Korn for Bell Labsbash: the Bourne Again shell, written in 1989 by Brian Fox for GNUzsh: the Z shell, written in 1990 by Paul Falstad, released open source under the MIT license
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
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:
-o or --options, specify the short-form options that can be supplied to this program-l or --long, specify the long-form options that can be supplied to this program
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
* matches any string of characters? matches any single character. file.?, for instance, would match file.c and file.o but not file.cpp[abc] will match a single character, either a, b, or c.
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.
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
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}.
An rcfile located in /etc will load for any user on the machine.
An rcfile located in ~/ will load for only that user.
When a shell is a login shell, it will source /etc/zprofile and ~/.zprofile in that order.
When a shell is a interactive shell, it will source /etc/zshrc and ~/.zshrc in that order.
Regardless of the shell, zsh will source /etc/zshenv and ~/.zshenv in that order.
Trace execution of files sourced on startup:
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
Parameter Expansion Flags
For reference, see the Zsh documentation for parameter expansion flags
${parameter:-word}
- If parameter is unset or null, the expansion of word is substituted. Otherwise, the value of parameter is substituted.
${parameter:=word}
- If parameter is unset or null, the expansion of word is assigned to parameter. The value of parameter is then substituted. Positional parameters and special parameters may not be assigned to in this way.
${parameter:?word}
- If parameter is null or unset, the expansion of word (or a message to that effect if word is not present) is written to the standard error and the shell, if it is not interactive, exits. Otherwise, the value of parameter is substituted.
${parameter:+word}
- If parameter is null or unset, nothing is substituted, otherwise the expansion of word is substituted.
${(P)parameter}
Normally, ${parameter} will expand to become the value of the variable whose identifier is parameter. By prepending the (P) parameter expansion flag, the shell will instead perform two dereferences. The value returned will be the value of the identifier whose name is stored in ${parameter}.
for parameter in XDG_{DATA,CONFIG,CACHE}_HOME; {
print "${parameter} -> ${(P)parameter}"
}
XDG_DATA_HOME -> ~/.local/share
XDG_CONFIG_HOME -> ~/.config
XDG_CACHE_HOME -> ~/.cache
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
Check the user
# 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 |
Arithmetic Evaluation
number=4
if (( number < 5 )); then
print "Number is less than five"
else
print "Number is not less than five"
fi
Output
Number is less than five
In order to perform arithmetic operations, surround variable names, integers, and operators in a ((...)) double quotations, like this:
Adding to 1 the number 2
value=1
((value+=2))
print ${value}
3
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
If you're on macOS, you're likely using BSD's ls. To colorize the output of ls, include the -G flag or add typeset -xg CLICOLOR=1 to your runtime configurations.
If you're using GNU's ls, supply the --color=auto argument to colorize the output of 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
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.
- The dollar sign
$ retains its special meaning. - The backtick ` retains its special meaning.
- A back-slash
\ only retains its special meaning when escaping a $, `, " or a newline character.
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
Prompt for input with Write some text: , save to variable variable
read -r 'variable?Write some text: '
Prompt for password, save to variable
# Save the result in the variable 'secret'
read -rs 'secret?Password:'
print "You entered ${secret}"
Pass each word from piped input into array words
print "alpha bravo charlie" | read -A words
print -l ${words}
alpha
bravo
charlie
Imagine you need to parse a file. You might want to read each line into an array before performing any operations on the underlying data.
Suppose file.txt contains the following text:
The array you seek to create can take one of two forms, depending on whether or not you'd like to elide the empty lines in file.txt:
In the end, the difference in the resulting array is a result of whether or not the parameter expansion flag (@) is provided during parameter expansion.
Read each line of file.txt into an array
text=$(<file.txt)
lines=(${text// /\\ })
Save contents of /dev/stdin to variable text
Printing all of the files in a directory
Print the names of subdirectories found within packages installed to /usr/local/opt:
print -l /usr/local/opt/*/*(/:t) | sort | uniq
Looping
While loops
typeset -i index=0
while (( ${index} < 5 )); do
print ${index}
# Lame variable increment
index=$((index+1))
# L33t variable increment
((index+=1))
done
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};
done
integer -p i
Output
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};
done
}
integer -p i
Output
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
Print the last 3 numbers
val='0123456789'
print ${val:(-3)}
789
Substring Matching
If you're looking for a way to remember these, there's a trick I use:
Look down at your keyboard
# is on the left, so it cuts off the left-side.$ looks like an S, so it's the string.% is on the right, so it cuts off the right-side.
string#pattern: Delete the shortest possible match of pattern from the front of string.string##pattern: Delete the longest possible match of pattern from the front of string.string%pattern: Delete the longest possible match of pattern from the end of string.string%%pattern: Delete the longest possible match of pattern from the end of string.
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)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]}
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
!! the previous command and all arguments
!* the previous command's arguments
!^ the previous command's first argument
!$ the previous command's last argument
!:2 the previous command's second argument
!^-: all the arguments of the previous command except for the last argument
!:-: the previous command except the last argument
!# the current command typed thus far
!grep: the most recent command starting with grep
!?string?: the most recent command containing string
!-2 the penultimate command
!#:0 the command being typed
!#:2 the second argument of the current command being typed
Next, attached below are expansions for arguments outside the context of command history
$_ an environment variable storing the last argument of the previous command
$$ the process ID itself
$* a string containing all arguments entered on the last command
$1 the first argument supplied to a command (if accessing command arguments from within a function script)
$2 the second argument supplied to a command (if accessing command arguments from within a function or script)
$@ all arguments supplied to a command (if accessing command arguments from within a function or script)
$? the return value of the previous command
$- the current options set for the shell (the single letter option names concatenated into a string)
Copy the last command to your pasteboard
Reference the first argument of the previous command
print first second third
print !^
# => "first"
Reference the last argument of the previous command
print first second third
print !:$
# => "third"
Reference the second argument of the previous command
print 'first' 'second' 'third'
print !:2
print 'second'
second
Reference all arguments of previous command, excluding the last argument
print first second third
print !:^-
# => This command would call `print 'first' 'second'`
Reference the second-to-last command
print 'three'
print 'two'
print 'one'
!-2 # This command would call `print 'two'`
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:
Matrix Transposition
You can use the -C and -a arguments to the print builtin to transpose
a matrix of words. I'm not sure when you'd need to do this, but in any event, here is how it's done.
Transpose the rows and columns of a matrix:
Before the transposition:
typeset -a words=(
'one'
'two'
'three'
'four'
'five'
'six'
'seven'
'eight'
'nine'
'ten'
)
# Specify the number of columns
typeset -i cols=2
print -C ${cols} ${words}
one six
two seven
three eight
four nine
five ten
After the transposition:
```shell
# Update the number of columns
cols=$(( ${#words} / ${cols} ))
print -C ${cols} -a ${words}
```
<samp class="output">one two three four five
six seven eight nine ten
Directory Expansion
~+: Expands to $PWD~-: Expands to $OLDPWD
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"
List the last 10 commands in history
List commands number 800 through 850 in history
List all commands in history that started with sudo
Globbing
Remove files that haven't been accessed in more than 1 day
# For files (not directories) in `~/tmp`, list those
# that haven't been accessed in at least 1 day
for file in ~/tmp/**/*(.ad+1); do
rm ${file}
done
# `(.)` select files, but not directories
# (ad+1) access time, >1 (d)ays from the present moment
Select all files in the current directory ending in a number, no matter how many digits! 🤯
./file0
./file12
./file001
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
Filter a key from an associative array:
typeset -A map=([a]=alpha [b]=bravo [c]=charlie)
typeset -a filter=(b)
print -- ${#${(k)foo}}
print -- ${#${(k)map:|filter}}
The output is the following:
(s) Split a string into an array, separating array entries on the occurance of a delimiter, which is removed from the elements. The delimiter can be specified by placing it within any of the following matching pairs: (...), {...}, [...], or <...>
string="one:two:three"
print -l ${(s<:>)string}
one
two
three
(j) Join an array into a string, uniting each entry with a delimiter, which is inserted between the elements in the string. The delimiter can be specified by placing it within any of the following matching pairs: (...), {...}, [...], or <...>
list=(one two three)
print ${(j<, >)list}
one, two, three
(W): Same as (w), but where empty words between repeated delimiters are also counted
Globbing
zsh is capapable of some very powerful globbing. Without setting any options, you can recursively iterate through directories with **/*.
Glob Options
setopt NULL_GLOB: If a glob pattern is not matched, don't return an error, return instead an empty string.
setopt EXTENDED_GLOB will enable special glob patterns, examples provided below:
# Select any file in any directory, whose parent directory is not 'src', 'bin', or 'lib'
./*/(*~src~bin~lib)/*(.); do
Included below are some features of the extended glob option:
^x Matches anything except the pattern x. This means that ^foo/bar will search directories in the present working directory except for ./foo for a file named bar
Glob Qualifiers
If you're interested in learning more, the Zsh documentation on glob qualifiers is a great place to read further.
Here are some flags below:
(:a): return each globbed file's absolute path.
print ./*(:a)
# ./example.txt => /Users/tommy/example.txt
(:P): return each globbed file's absolute path, resolved of any symbolic
links
print ./*(:P)
# ./example.txt => /Users/tommy/example.txt => /Users/tommy/real/example.txt
(:A): return each file's absolute paths, trying to resolve symbolic links, falling back on the absolute file to the symlink file itself if the directory it points to does not exist.
# [ Using (:A) ]
ln -s /fake/path ~/Desktop/example
print ~/Desktop/example(:A)
# => /Users/austin/Desktop/example
# [ Using (:P) ]
print ~/Desktop/example(:P)
# => /fake/path
(:e): strip everything but the extension from each globbed file
print ./*(:e)
# ./example.txt => txt
(:r): strip the extension suffix
print ./*(:r)
# ./example.txt => ./example
(:t): strip all of the leading directories from the filepath
val="./path/to/file.txt"
print "${val} => ${val:t}"
# ./path/to/example.txt => example.txt
(:h): strip one trailing pathname component from the filepath
val="./path/to/file.txt"
print "${val} => ${val:h}"
./path/to/file.txt => ./path/to
Consider a case where you would like to print the absolute path to the file that is currently being executed (or sourced). You can use the ZSH_SCRIPT environment variable to derive this value, and you can use these expansion
* Option two
```shell
echo ${${(%):-%N}:A}
```
Output
<samp class="output">/usr/local/bin/filename.sh
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
on Name
oL Filesize
oa Time Accessed
om Time Modified
oc Time Created
odon Sort by names for files within the same directory
*(^-oL)' Sort all files by file size in descending order, resolving any symbolic links
Print all of the directories in descending order of size, in an escaped format to be re-usable by the shell
Select the largest regular file within a directory
# 'L': (normally) sort by length (of file, i.e. its size), ascending
# (Using ascending order, and picking the last element)
print ./*(.DoL[-1])
# (Using descending order, and picking the last element)
# 'O': reverse order
print ./*(.DOL[1])
Select all files larger than 2MB in a directory
# 'm' (megabytes) (and 'k' for kilobytes)
# '-' (smaller than 2)
print ./*(.Lm-2)
Select the most recently modified file within a directory
Select all files modified within the last hour
# 'M' for Months
# 'w' for weeks
# 'h' for hours
# 'm' for minutes
# 's' for seconds
# '-': modified less than '#' hours ago
# '+': modified more than '#' hours ago
print -l ./*(.mh-1)
Open all files created within the last 2 days
open ./path/to/dir/*(.c-2)
Add each directory to the ${folders} array, but only if it exists
# Using
# (N) enable null glob
# (/) only match an existing directory
typeset -a folders
folders=( /usr(/N) /bin(/N) /asdf(/N) )
print -l ${folders}
/usr
/bin
Checking if a Command Exists
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"
else
print "Command not found"
fi
# [ Right way, note the (( parentheses )) ]
if (( ${+commands[brew]} )); then
print "Command exists"
else
print "Command not found"
fi
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
<words.txt>
the day is sunny the the
the sunny is is
Printing the count of each word occuring in words.txt in descending order
words=($(<words.txt))
# Create an array to store the word and its count
declare -a count
# For each (u)nique word
for word in ${(u)words}; do
# Add "<#> <word>" to the array
count+=("$(grep -c ${word} <<< "${(F)words}") ${word}")
done
# Print the results, sorted (n)umerically
# (O)pposite of ascending order
for result in ${(On)count}; do
print ${result}
done
Solution using command line tools
# Short form
tr -s ' ' '\n' < words.txt \
| sort \
| uniq -c \
| sort -r
# Long form
tr --squeeze ' ' '\n' < words.txt \
| sort \
| uniq --count \
| sort --reverse
Split each word in the string by the delimiter :
string="alpha::charlie"
# excluding the empty index value in the middle
array1=(${(s_:_)string}) # using '_' as argument separator
array1=(${(s[:])string}) # using '[]' as argument separator
print ${#array1} # => '2'
# including the empty index value in the middle
array2=("${(@s_:_)string}") # using '_' as argument separator
array2=("${(@s[:])string}") # using '[]' as argument separator
print ${#array2} # => '3'
Create an array out of the lines outputted by a command
print -l ${(f)"$(networksetup -getinfo Wi-Fi)"}
Extract the second line of output from a command
print ${${(f)"$(networksetup -getinfo Wi-Fi)"}[2]}
Append .old to each scalar in the array
files=(
./one.txt
./two.txt
./three.txt
)
print -l ${files/%/.old}
# => ./one.txt.old
# => ./two.txt.old
# => ./three.txt.old
Prepend old. to each scalar in the array
people=(
man
woman
maid
)
print -l ${files/#/old.}
# => old.man
# => old.woman
# => old.maid
Print each unique word in a paragraph
string="this sentence is a sentence
this line is part of the paragraph
and this line is the end"
words=(${=string})
print -l ${(u)words}
# => this sentence is a line part of the paragraph and end
Print each word in lexicographic order
string="third fourth Second First"
words=(${=string})
print ${(o)words}
# => First Second Third Fourth
Given a string that includes tabs, spaces, and newlines, return an array of just the words
string=$'first\tsecond\nthird fourth fifth sixth'
array=(${=string})
print ${#array} # 6
Passing escape sequences to a string
print $'name:\tAustin Traver\nid:\t1234'
# => name: Austin Traver
# => id: 1234
Check if a variable is set
# If variable "var" is set
if [[ -v var ]] {
print "Variable is set"
} else {
print "Variable is not set"
}
Warning: Don't expand the value of var (e.g. ${var}) or the statement won't work
Check if a variable is either unset, or is set, but is the empty string
if [[ -z ${var} ]] {
print "Variable 'var' is either an unset variable or is a variable whose value is set to the empty string"
}
C-style for loop
for ((i=0; i<10; ++i)); do
print ${i}
done
whence
The whence command is very useful, and can replace many common commands
whence -v is equivalent to typewhence -p is equivalent to pathwhence -c is equivalent to whichwhence -c is equivalent to wherewhence is equivalent to command -v
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.
Storing the contents of a here-doc in file.txt:
if [[ true ]]; then
<<-EOF > file.txt
1 leading tab
2 leading tabs
3 leading tabs
EOF
fi
Using a here-doc to avoid printing leading tabs to stdout:
if [[ true ]]; then
cat < =( <<-EOF
this output is split along multiple lines
as such, but they strip any leading tabs
but not leading spaces
EOF
) >&1
fi
Assigning a heredoc to a variable using Zsh:
read -r -d '' VARIABLE <<-EOF
the first line
the second line
the third line
EOF
print -- ${ VARIABLE }
the first line
the second line
the third line
Here-String
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...
Supply the string hello world\n as standard input to the current command
grep 'world' <<< 'hello world'
Create the file hello.txt with the following contents
Contents
Command
<<< $'hello\nworld' > hello.txt
Supply a multi-line string (including a trailing line feed) as standard input to the current command
Input string:
Command:
Output:
hello world
its me
computer
Supply a multi-line string (excluding a trailing line feed) as standard input to the current command
cat =(<<<$'hello world\nits me\ncomputer')
here-string with and without a trailing newline (using tmp file substitution)
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.
For the following example, assume file.txt contains the following:
Hello, my name is $USER
and my favorite directory is $HOMEExpanding parameters as if they were variables in file.txt:
info=${(e)$(<./file.txt)}
print ${info}
Output:
Hello, my name is austin
and my favorite directory is /Users/austin
exit vs. logout
exit will close all shells, interactive, non-interactive, login, non-login
logout will only close out of a login shell, even if it's interactive
return will stop the execution of the script that made the call, but exit will close the shell that sourced that file to begin with
Brace Expansion
Multiple mid-word character substitutions
print h{a,e,i,o,u}p
# => hap hep hip hop hup
Repeating a string multiple times
print 'woah'{,}
# woah woah
print 'woah'{,,}
# woah woah woah
print 'woah'{,,,}
# woah woah woah woah
Back-to-back expansions with filename generation
print -- {/,/usr/}{bin/*,sbin/*}
Generating ranges of numbers
01 02 03 04 05 06 07 08 09 10
```shell
print {01..10..3}
```
<samp class="output">01 04 07 10
```shell
print {a..z}
```
<samp class="output">a b c d e f g h i j k l m n o p q r s t u v w x y z
```shell
print {a..z..3}
```
<samp class="output">a d g j m p s v y
```shell
left=1
right=9
print {${left}..${right}}
```
<samp class="output">1 2 3 4 5 6 7 8 9
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 ))
a=5
b=6
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
Print two lines using C quotes $'...'
Print the character corresponding with the hex value 0x41
Print the character corresponding with the UTF-8 character code u+7231
Print the character corresponding with the UTF-8 character code U+1f602
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=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'
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
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.
print "6 / 8 = $(( 6 / 8 ))"
6 / 8 = 0
print "6 / 8 = $(( 6 / 8.0 ))"
6 / 8 = 0.75
File Descriptors
<&-: Close the standard input.
1>&-: Close the standard output.
2>&-: Close the standard error.
<&p: Move the input from the coprocess to stdin
>&p: Move the output from the coprocess to output
2>&1: Redirect standard error to standard output
1>&2: Redirect standard output to standard error
&> file.txt: Redirect both standard output and standard error to file.txt
Redirect output and error to different files
func() {
print 'output' >&1
print 'error' >&2
}
# [ Version 1 ]
func 1>out.txt 2>err.txt
# [ Version 2 ]
1> out.txt 2> err.txt func
Custom File Descriptor
You can create your own file descriptor number and have it direct to any file you'd like.
- Create file descriptor
3 and point it to /dev/null
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
Create a file descriptor variable named fd and have the shell assign a free file descriptor (starting from 10+) and save it as the value of that variable
{abc}>&1: create a file descriptor "abc" that is a duplicate of file descriptor 1
>&$abc: write to the file descriptor "abc"
<&$abc: read from the file descriptor "abc"
{abc}>&-: close file descriptor "abc"
# 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.
Delete Dangling Symlinks
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.
(@): Only symlinks
(-@): Only broken symlinks
(D): Match .hidden dot files
Deleting all dangling symlinks:
# '-@' only broken symlinks
# 'D' include .dotfiles
rm -- ./*(-@D)
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.
Remove from array any element that matches the pattern:
# *r*: strings containing the letter 'r'
array=('number one' two three)
output=(${array:#*w*})
typeset -p output
typeset -a output=( 'number one' three )Remove from 'array' any element that does not match the pattern pattern\*
# *r*: strings containing the letter 'w'
array=('number one' two three)
output=(${(M)array:#*w*})
typeset -p output
typeset -a output=( two )Remove any line from 'whois' that doesn't start with 'CIDR'
ip='8.8.8.8'
print -- ${(M)${(@)${(f)${"$(whois ${ip})"}}}:#CIDR*}
CIDR: 8.0.0.0/9
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
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
a+=1
typeset a
Output
Flags to state variable type
-F [ name[=value] ... ]: set name as floating point (decimal notation)-E [ name[=value] ... ]: set name as floating point (scientific notation)-i [ name[=value] ... ]: set name as an integer-a [ name[=value] ... ]: set name as an array-A [ name[=value] ... ]: set name as an associative array
Flags to state variable properties
typeset -r [ name[=value] ... ]: mark name as read-onlytypeset +r [ name[=value] ... ]: remove the read-only property of NAMEtypeset -x [ name[=value] ... ]: mark name as exportedtypeset -g [ name[=value] ... ]: mark name as globaltypeset -U [ name[=value] ... ]: convert array-type variable name such that it always contains unique-elements only
Flags to modify command output
typeset -l [ name[=value] ... ]: print value of name in lower-case whenever expanded
typeset -u [ name[=value] ... ]: print value of name in upper-case whenever expanded
typeset -H [ name[=value] ... ]: suppress output for typeset name if variable name has already been assigned a value
typeset -p [ name[=value] ... ]: print name in the form of a typeset command with an assignment, regardless of other flags and options. Note: the −H flag will still be respected; no value will be shown for these parameters.
typeset -p1 [ name[=value] ... ]: print name in the form of a typeset command with an assignment, regardless of other flags and options. Note: arrays and associative arrays are printed with newlines between indented elements for readability.
Matching a Certain Type
Matching a Certain Pattern
- Print environment variables whose names match the pattern
foo
foo_fighters
food
- Print variable and its corresponding value for environment variables whose names match the pattern
foo=bar
foo_fighters=awesome
food=(my life)
- Print variables'
typeset options, its name, and its assigned value, for each matching the pattern:
typeset foo=bar
typeset foo_fighters=awesome
typeset -a food=( my life )
Print all keys in an associative array that don't start with foo
print ${(k)example:#foo*}
Print all keys in an associative array that do start with foo
print ${(Mk)example:#foo*}
Print all values in an associative array that don't start with foo
print ${(v)example:#foo*}
Print all values in an associative array that do start with foo
print ${(Mv)example:#foo*}
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
Format text to be bolded
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.
Completions
Useful Oreilly Resource
compsys
The new system to use is compsys. It has a manpage zshcompsys(1) as well.
The old system was called compctl and its manpage is in zshcompctl(1). The
first paragraph is dedicated to recommending you just head back over to the
new zshcompsys(1) system.
Completion Functions
Let's say our program is called hello.
Here's what will happen:
- You write a completion function, typically
_<cmd-name>
_hello() {
# You write your code here
}
- You bind your function to a command
- Whenever you press
<Tab> after hello, _hello will be called.
Whenever you want to throw out possible completions, you'll use one of the following utility functions(in this post):
compadd
- Reference:
man zshcompwid
If you want to have this:
hello <Tab>
# => cmd1 cmd2 cmd3
You'll write this:
_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:
'-o[description]' for an option'<argument number>:<message>:<what to do>' for an argument
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:
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> ...
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:
%2
The last job with job ID 2
%vi
The last job whose command line begins with vi
%?grep
The last job whose command line contains grep
%%
The current job.
%+
Equivalent to %%.
%-
The previous job.
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
Print the closest match to violet (#AFAFFF) among the 256 terminal colors
zmodload zsh/nearcolor
print -P '%F{#AFAFFF}Violet%f`
Print the ANSI escape sequence for the closest match to violet (#AFAFFF) among the 256 terminal colors
zmodload zsh/nearcolor
print -- ${(V%)$(<<<'%F{#F9FFB3}yellow%f')}
Output
A few parameter expansion flags are used in this print statement:
Multios
- See output on
stdout but save to file.txt as well
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
Zsh supports the traditional syntax for conditional statements and for loops. However, they also provide some more modern versions of each, as demonstrated below:
One line if statement, single command:
if [[ ${USER} == 'austin' ]] print "That's him"
Multi-line if statement, any number of commands:
if [[ ${USER} == 'austin' ]] {
print "That's him"
} elif [[ ${USER} == 'valerie' ]]
print "That's her"
} else {
print "That's nobody important"
}
One-liner for loop, with a single statement (Note: The omission of the do and done keywords is required when using this form)
for letter ('a' 'b' 'c'); print ${letter}
Output
Multi-line for loop, any number of statements (Note: The omission of the do and done keywords is required when using this form, as are the curly braces)
for letter ('a' 'b' 'c'); {
print ${letter}
print '-'
}
Output
Alternative way of creating a loop containing multiple consecutive commands (Note: The inclusion of the end keyword at the bottom is required, as is the ommission of the do and done keywords):
foreach letter (
'a'
'b'
'c'
)
print "1: ${word}"
print "2: ${word}"
end
Output
1: a
2: a
1: b
2: b
1: c
2: c
Syntax for short-form of while loop
# Keep sleeping until the server starts running
while [[ $(curl http://127.0.0.1 -- &> /dev/null)$? -eq 7 ]] {
sleep 0.2
}
print "Server is now running"
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
calls time self name
- 2 22.18 11.09 45.03% 22.18 11.09 45.03% compaudit
- 1 32.66 32.66 66.29% 10.48 10.48 21.27% compinit
- 5 0.77 0.15 1.56% 0.77 0.15 1.56% add-zsh-hook
- 1 0.45 0.45 0.90% 0.45 0.45 0.90% bashcompinit
- 1 0.28 0.28 0.56% 0.28 0.28 0.56% is-at-least
- 1 0.15 0.15 0.31% 0.15 0.15 0.31% (anon)
- 1 0.09 0.09 0.19% 0.09 0.09 0.19% compdef
- 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}
fi
end
Pretty-Printing Associative Array
Zsh Hashed Commands
Instead of searching the path each time for a command, Zsh hashes commands
hash
enable
enable a builtin command
enable an alias
enable a function
disable
unhash
You can use the unhash tool to remove almost any type of command from your current shell.
Remove a command
Remove an alias
Remove a function
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.
Navigating The Terminal
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.
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
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
SIGTERM (15): Tells a program to stop, in order to allow the program to handle its termination gracefully. Can be caught, handled, or ignored.
SIGINT (2): Used to interrupt a program that is running. It is the same as the SIGTERM signal, but it explicitly refers to an interruption that was called from the terminal. Can be caught, handled, or ignored.
SIGQUIT (3): Similar to SIGTERM but it will generate a core dump. Can be caught, handled, or ignored.
SIGSTOP (17): Temporarily stop a program. Cannot be caught, handled, or ignored.
SIGTSTP (18): Sends the program a signal, telling it to temporarily stop. Unlike SIGSTOP, it can be caught, handled, or ignored.
The Foreground & Background
The jobs Program
The jobs program lets you see information about the current jobs running from this terminal.
View the jobID, job status, and call-command
Additionally report the PID of each job
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 a process by name
Kill a process running on a specific port
Send the SIGTERM (15) to process 123
Send the SIGTERM (15) signal to process 123 & process 456
Send the SIGINT (2) signal to process 123
Send the SIGSTOP () signal to process 123
Send the SIGINT (2) signal to job ID # 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
-k Use 1 kilobyte as the default size-unit, instead of half a kilobyte (not sure why this isn't standard...)-h Print the response in human readable output.-c-s
Customization
Custom Bash Prompt
The bash prompt is actually a collection of several prompts.
PS1: The primary bash prompt, defaults to include the following bash escape sequences.
\h: The hostname Austins-Macbook-Pro\W: The basename of the current working directory ~\u: The username austintraver\$: A $ char, unless the UID is not 0, then it's #
Personally I like the way it looks when I ssh into my virtual private server. If you want to try it out, you can run the following command in your terminal.
Modify the machine's hostname (on macOS):
sudo scutil --set HostName HOSTNAME
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
# 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!
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
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 website in Safari
open -a Safari 'https://google.com'
Open with the default text editor
Launch a new instance of the application
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.
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.
Useful Flags
- The
-t flag specifies how many seconds to stay awake - The
-w flag will wait until the program with the given PID finishes before reenabling sleep. - The
-u flag will signal wake via user activity, keeping a computer awake as if someone jiggled the mouse or pressed a key.
Following Symlink Directories
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'
fi
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'
fi
success
Operator Precedence
Proof that || has operator precedence over &&
Example 1:
if true && false || true; then
print 'success'
else
print 'failure'
fi
success
Example 2:
if true || false && true; then
print 'success'
else
print 'failure'
fi
success
zmv
The zmv command is an alternative to mv, and can be loaded into the shell
environment using the autoload command.
Usage
Rename a section of a filename, i. e. example.1.{txt,conf,db} or 12345.1.{wav,ogg,mp3} and
change the 1 to a 2 in the filename:
# would rename x.0001.y to x.2.y.
zmv -n '(*.)(<->)(.[^.]#)' '$1$(($2+1))$3'
zmv -n '(*.0#)(<->)(.[^.]#)' '$1$(($2+1))$3'
Change files to lowercase:
Serially rename all files (e.g.: foo.foo -> 1.foo, fnord.foo -> 2.foo, etc.):
ls *
# 1.c asd.foo bla.foo fnord.foo foo.fnord foo.foo
c=1 zmv '*.foo' '$((c++)).foo'
ls *
# 1.c 1.foo 2.foo 3.foo 4.foo foo.fnord
Rename file.with.many.dots.txt by substituting dots (except for the last
one!) with a single space:
touch {1..20}-file.with.many.dots.txt
zmv '(*.*)(.*)' '${1//./ }$2'
Remove the first 4 chars from a filename
zmv -n '*' '$f[5,-1]' # NOTE: The "5" is NOT a mistake in writing!
Rename names of all files under the current directory to lowercase, but keep
the directory names themselves intact.
zmv -Qv '(**/)(*)(.D)' '$1${(L)2}'
Replace all 4th character, which is "1", with "2" and so on:
autoload -U zmv
zmv '(???)1(???[1-4].txt)' '${1}2${2}'
Remove the first 15 characters from each filename:
touch 111111111111111{a-z}
zmv '*' '$f[16,-1]'
Replace spaces (any number of them) with a single dash in filenames:
zmv -n '(**/)(* *)' '$1${2//( #-## #| ##)/-}'
Clean up filenames and remove special characters:
zmv -n '(**/)(*)' '$1${2//[^A-Za-z0-9._]/_}'
Lowercase all extensions (i.e.: *.JPG) including those found in
subdirectories:
zmv '(**/)(*).(#i)jpg' '$1$2.jpg'
Remove leading zeros from file extension:
ls
# filename.001 filename.003 filename.005 filename.007 filename.009
# filename.002 filename.004 filename.006 filename.008 filename.010
zmv '(filename.)0##(?*)' '$1$2'
ls
# filename.1 filename.10 filename.2 filename.3 filename.4 filename.5
# filename.6 ...
Renumber files:
ls *
# foo_10.jpg foo_2.jpg foo_3.jpg foo_4.jpg ...
zmv -fQ 'foo_(<0->).jpg(.nOn)' 'foo_$(($1 + 1)).jpg'
ls *
# foo_10.jpg foo_11.jpg foo_3.jpg foo_4.jpg ...
Adding leading zeros to a filename:
# 1.jpg -> 001.jpg, ...
zmv '(<1->).jpg' '${(l:3::0:)1}.jpg'
Add leading zeroes to files with a filename with at least 30 characters:
typeset c=1
zmv "${(l:30-4::?:)}*.foo" '$((c++)).foo'
Replace all spaces within filenames into underlines:
Change the suffix from *.sh to *.pl:
Add a .txt extension to all the files within ${HOME}:
zmv -Q '/home/**/*(D-.)' '$f.txt'
-. is to only rename regular files or symlinks to regular filesD is to also rename hidden files (files with names that start with .)
Only rename files that don't have an extension:
zmv -Q './**/^?*.*(D-.)' '$f.txt'
Recursively change filenames with characters contained in the character set
[?=+<>;",*-]':
chars='[][?=+<>;",*-]'
zmv '(**/)(*)' '$1${2//$~chars/%}'
Removing single quote from filenames (recursively):
zmv -Q "(**/)(*'*)(D)" "\$1\${2//'/}"
When a new file arrives (named file.txt) rename all files in order. For example,
file119.txt becomes file120.txt, file118.txt becomes file119.txt
and so on ending with file.txt being changed to become file1.txt:
zmv -fQ 'file([0-9]##).txt(On)' 'file$(($1 + 1)).txt'
Convert all filenames to be entirely lowercase:
Convert all filenames to be entirely uppercase:
Remove the suffix *.sh from all shell script files:
Uppercase only the first letter of all *.mp3 files:
zmv '([a-z])(*).mp3' '${(C)1}$2.mp3'
Copy the target README.md file into same directory as each Makefile:
zmv -C '(**/)Makefile' '${1}README.md'
Removing the single quotation character ' from filenames:
zmv -Q "(*'*)(D)" "\$1\${2//'/}"
Rename pic1.jpg, pic2.jpg, etc., into pic0001.jpg, pic0002.jpg, etc.:
zmv 'pic(*).jpg' 'pic${(l:4::0:)1}.jpg'
zmv 'pic(*).jpg' '$1/pic${(l:4::0:)2}.jpg'
Useful snippets