Expansion and Signals
Overview
This section will cover “shell expansion”, which allows you to pattern match file and directory paths as well as content from your history.
We’ll also briefly cover some basic process management using signals
and jobs
.
Some of the commands on this page require the “coreutils
” package (“brew install coreutils
”).
Expansion and “Globbing”
A glob is a simple way of pattern matching file or directory paths in your commands.
globs
are much more simple than a regualr expression
, and you’ve probably already used this feature before.
*
matches zero or more characters.?
matches exactly one character.[]
specify a ‘group’ of characters to match, optionally negated with!
.
Examples:
# list all markdown files in this directory:
ls *.md
# look for log files in the form of `log-N.log`, for any (single-character) value of `N`:
ls log-?.log
# search for any file named `the-letter-X.txt`, where `X` is an alpha character:
ls the-letter-[abcdefghijklmnopqrstuvwxyz].txt
# or, more simply:
ls the-letter-[[:alpha:]].txt
# or any version where the character is not (`!`) "q"
ls the-letter-[\!q].txt
The Wikipedia page is very short and has a good syntax table and some reference examples.
When you enter a command, zsh
will “expand” all the special directives in your command string and replace them with the actual values that match the patterns, BEFORE executing it as a shell command.
One common pattern that you may be familiar with is **
which would expand to all sub-directories:
# list all markdown files, anywhere under the current directory.
ls **/*.md
Another very useful globbing/expansion pattern is {}
:
# open three files at once, from a directory location i don't
# want to type out every time:
vim /very/long/path/to/my/project/file{A,B,C}.py -O3
The argument /very/log/path/to/my/project/{fileA,fileB,fileC}.py
expands to:
/very/log/path/to/my/project/fileA.py \
/very/log/path/to/my/project/fileB.py \
/very/log/path/to/my/project/fileC.py
!? and !!
Besides expanding “path” patterns, your shell also supports “history” expansion.
Two that I use frequently are !$
and !!
. These two patterns will let you quickly substitute part of your previous command into your next command:
!$
- This will be replaced with the last parameter from the previous command you ran.!!
- This will be replaced with the entire previous command.
Try it Out
Try the following sequence of commands:
mkdir test
ls !$
touch !$/file.txt
file !$
git add !$
history | tail -n 6
touch /etc/test.test
# touch: /ect/test.test: Permision denied
sudo !!
rm -f !$
# rm: /ect/test.test: Permision denied
sudo !!
Try running man zshall
and searching for HISTORY EXPANSION
and PATH EXPANSION
for
String Interpolation, Quoting, and escaping
Shells make heavy use of string concatenation and interpolation.
Simply putting two strings next to each other in a command will concatenate them:
echo "HELLO" "WORLD"
echo "HELLO ""WORLD"
A string that is surrounded in double-quotes ("..."
) supports interpolation, and requires escaping of some special characters. A string surrounded in single-quotes does not support interpolation, and you don’t have to escape anything (but you have to be careful if you want to use '
in your string).
# Print your prompt string in parentheses:
echo "MY PROMPT: ($PS1)"
# Run a command and include it in your string:
echo "Hello, $(whoami)!"
# Print the literal text "$PS1" (without interpolating the real value):
echo "MY PROMPT: (\$PS1)"
# or
echo 'MY PROMPT: ($PS1)'
Run man zshall
and search for QUOTING
Sometimes you want to pass a pattern or glob to another command, like find
:
# Say you are currently in a directory with two .txt files in it:
ls .
# file1.txt
# file2.txt
# Now, I want to use `find` to locate all .txt files under the directory `~/wiki`:
find ~/wiki -name *.txt
# find: file1.txt: unknown primary or operator
# find: file2.txt: unknown primary or operator
What happened here? Your shell expanded *.txt
BEFORE passing it to the command!
# this is not what we wanted:
find ~/wiki -name file1.txt file2.txt
We wanted to send the actual glob pattern string as an argument, not to expand it and pass those results as the arguments.
The way to fix this is by quoting the glob
pattern with single quotes.
find ~/wiki -name '*.txt'
If you use double-quotes, then expansion still happens! Use single-quotes to avoid expansion, or else escape the glob characters with a back-slash: "\*.txt"
.
Jobs
By default, when you run a command from the command line, it will run in the foreground - it takes control of your input and output for that terminal until it is completed.
Sometimes you want a long-running command to run in the background, where it will run asynchronously, and you can continue doing other work in that same terminal. Other times, you might want to suspend the program you are currently interacting with, drop back to the shell, do something else, then either resume interacting with your application or send it to the background.
ctrl-z
- Suspend the current task and drop back to the shell.jobs
- Command to print the number of suspended or background jobs you have active in your shell session.fg
- Resume the first suspended job.bg
- Send the first suspended job to the background.
Try it Out
Try the following sequence of commands and keybindings:
man ls
<ctrl-z>
less ~/.zshrc
<ctrl-z>
Now you have 2 suspended jobs:
jobs
# [1] - Stopped man ls
# [2] - Stopped less ~/.zshrc
If you type fg
it should resume the most recent job, which is less ~/.zshrc
:
fg
<ctrl-z>
If you want to resume the other job, look at it’s ID in the jobs
output. If we want to start job [1]
instead, we would use %1
as an argument to the fg
command:
fg %1
<ctrl-z>
Now try killing the man ls
job:
kill %1
jobs
# [1] - Terminated man ls
# [2] - Stopped less ~/.zshrc
jobs
# [2] - Stopped less ~/.zshrc
If the program we have suspended to the background is a long running process, that we want to run asynchronously while we do other things in the shell, we can use the bg
command to do that:
# most recent job to background
bg
# job 1 to background
bg %2
It doesn’t really make sense to send this process to the background, that’s something you would do for a long running script or other asynchronous task.
kill %2
Starting a Command in the Background
If you want to start a command and run it directly in the background, you can add a &
(single ampersand) at the end:
find / -name '*.txt'&
The example above would search the entire system for ‘.txt’ files, which could take a very long time! By ending the command with &
, it runs in the background.
While that command is running you will still be able to use you shell normally, but you may see the STDERR
output in your terminal. When the command completes, you will see the results printed to your STDOUT
.
When the background job is completely finished, it will print a message like:
[1] + exit 1 find / -name '*.txt'
You can find the running job with jobs
, and continue running it in the foreground with fg
, if you want.
Signals
A “signal” allows for manipulation from outside of the program or process. The most common things you might want to do are probably:
- Kill a process
- Kill a process (with fire)
- Stop or suspend a process
There are a several more, but we’re only covering the most basic ones.
Try running man signal
and browsing the documentation.
Your programs can also “trap” any these signals and add custom behavior to handle those events.
There are keybindings for a couple of the most common signals:
ctrl+c
- This will send theSIGINT
signal, which tells the command to clean up and exit.ctrl+\
- Sometimes the program hangs and doesn’t exit nicely when you sendSIGINT
. You can usectrl+\
to kill it harder, by sending theSIGQUIT
signal.
Use “ctrl+c
” when you need to quit out of a program that is not responding. Try to wait for the process to exit normally but, if it’s really stuck, you can use “ctrl+\
” to send it the SIGKILL
signal. Doing this may possibly result in the application not cleaning up properly before exiting, which may cause issues when you try to run it again.
ps, kill, pkill
How do you send a specific signal to an application? If there is no existing keybinding, or you don’t know what that key is, you can use the kill
command. Despite it’s name, kill
allows you to send any signal code, not just SIGKILL
.
For example, if you really, really want to kill a process and completely nuke it, you can send it SIGKILL
instead of the normal SIGINT
or SIGQUIT
signals:
ps aux | grep vim
# dave 9898 ... (more stuff truncated) /usr/bin/vim ...
# dave 12345 ... (more stuff truncated) grep vim
kill -9 9898
We can use the ps
command to find the process ID (pid
) for a running command, then we can pass that pid
to the kill
command to target that running process. The -9
means send the signal with ID=9, which is SIGKILL
.
To find a process ID, or to look to see if a process is running somewhere, use ps aux
and then pipe that to grep to filter out the command that you want.
ps aux | grep vim
# dave 9898 ... (more stuff truncated) /usr/bin/vim ...
# dave 12345 ... (more stuff truncated) grep vim
ps aux | grep [v]im
# dave 9898 ... (more stuff truncated) /usr/bin/vim ...
You may notice the first version of the command returned 2 processes: the vim
command we searched for, but also the grep
command that we just typed.
To omit the grep
command from the results, we use a glob “[]
” around one character in the grep pattern string. This changes the text in the ps
output to “[v]im
”.
Grep can use “[v]im
” (a “glob”) to match “vim
” in the output, but it will not match against the literal string “[v]im
” from the output. It’s a useful trick for when you are scripting things that use ps
to find a process.
Sometimes, you know the name of a command and you just want to nuke it. You can use the pkill
command to match a process name, based on a regular expression, then send it a signal:
pkill -9 -f vim
The parameter after the -f
is the search pattern, and the -9
sends the SIGKILL
signal.
DANGER! Be careful with pkill
. If your search pattern matches more processes, besides the one you intended, they will all get killed too!