A topic closely related to pattern substitution is pattern removal. Let's face it, pattern removal is basically the same as replacing a pattern with nothing.
If pattern removal had exactly the same functionality as pattern substitution, we would not need it. However, pattern removal has a few cool tricks that would be difficult or even impossible to do with pattern substitution.
Pattern removal has two options: removing matched pattern prefixes or suffixes. In simpler words, it allows you to remove stuff either from the beginning or the end. It also has an option to stop after the first matched pattern, or to continue up until the last.
Without a good example, this might be a bit too abstract (it definitely was for us the first time we encountered this). However, there is an excellent example here: it all has to do with files:
reader@ubuntu:/tmp$ touch file.txt
reader@ubuntu:/tmp$ file=/tmp/file.txt
reader@ubuntu:/tmp$ echo ${file}
/tmp/file.txt
We've created a variable that contains a reference to a file. If we wanted the directory, or the file without the directory, we could use either basename or dirname, as follows:
reader@ubuntu:/tmp$ basename ${file}
file.txt
reader@ubuntu:/tmp$ dirname ${file}
/tmp
We could also achieve this with parameter expansions. The syntax for prefix and suffix removal is as follows:
${parameter##word}
Remove matching prefix pattern.
${parameter%word}
${parameter%%word}
Remove matching suffix pattern.
For our ${file} variable, we can use parameter expansion to remove all directories and only keep the filename, as follows:
reader@ubuntu:/tmp$ echo ${file#/}
tmp/file.txt
reader@ubuntu:/tmp$ echo ${file#*/}
tmp/file.txt
reader@ubuntu:/tmp$ echo ${file##/}
tmp/file.txt
reader@ubuntu:/tmp$ echo ${file##*/}
file.txt
The difference between the first and second command is minimal: we're using the asterisk wildcard that can match on anything, zero or more times. In this case, since the value of the variable starts with a forward slash, it does not match. However, as soon as we get to the third command, we see the need to include it: we need to match everything we want to delete.
In this case, the */ pattern matches on /tmp/, whereas the / pattern only matches on the first forward slash (as the result of the third command clearly shows).
It is good to remember that in this instance, we're merely using parameter expansion to replace the functionality of the basename command. However, if we were not dealing with file references, but (for example) underscore delimited files, we could not achieve this with basename, and parameter expansion would come in quite handy!
Now that we've seen what we can do with prefixes, let's look at suffixes. The functionality is of the same order, but instead of parsing from the start of a value, we're now looking at the end of the value first. We could use this, for example, to remove the extension from the file:
reader@ubuntu:/tmp$ file=file.txt
reader@ubuntu:/tmp$ echo ${file%.*}
file
This allows us to grab the filename, without the extension. This might be desirable if there is some logic in your script that can be applied to this part of the file. In our experience, this is more common than you think!
For example, you might imagine backups that have a date in the filename that you'd like to compare to today's date, to ensure a backup was successful. A little bit of parameter expansion can get you to your desired format, so the comparison of dates is then trivial.
Just as we were able to replace the basename command, we can do the reverse with suffix pattern removal to find the dirname, as follows:
reader@ubuntu:/tmp$ file=/tmp/file.txt
reader@ubuntu:/tmp$ echo ${file%/*}
/tmp
Again, these examples mostly serve for educational purposes. There are many situations in which this could be useful; since these are very diverse, it is hard to give an example that is interesting for everyone.
The situation we introduced regarding backups, however, might be relevant for you. As a basic script, it would look something like this:
reader@ubuntu:~/scripts/chapter_16$ vim check-backup.sh
reader@ubuntu:~/scripts/chapter_16$ cat check-backup.sh
#!/bin/bash
#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-12-16
# Description: Check if daily backup has succeeded.
# Usage: ./check-backup.sh <file>
#####################################
# Format the date: yyyymmdd.
DATE_FORMAT=$(date +%Y%m%d)
# Use basename to remove directory, expansion to remove extension.
file=$(basename ${1%%.*}) # Double %% so .tar.gz works too.
if [[ ${file} == "backup-${DATE_FORMAT}" ]]; then
echo "Backup with todays date found, all good."
exit 0 # Successful.
else
echo "No backup with todays date found, please double check!"
exit 1 # Unsuccessful.
fi
reader@ubuntu:~/scripts/chapter_16$ touch /tmp/backup-20181215.tar.gz
reader@ubuntu:~/scripts/chapter_16$ touch /tmp/backup-20181216.tar.gz
reader@ubuntu:~/scripts/chapter_16$ bash -x check-backup.sh /tmp/backup-20181216.tar.gz
++ date +%Y%m%d
+ DATE_FORMAT=20181216
++ basename /tmp/backup-20181216
+ file=backup-20181216
+ [[ backup-20181216 == backup-20181216 ]]
+ echo 'Backup with todays date found, all good.'
Backup with todays date found, all good.
+ exit 0
reader@ubuntu:~/scripts/chapter_16$ bash check-backup.sh /tmp/backup-20181215.tar.gz
No backup with todays date found, please double check!
To illustrate this, we're touching dummy backup files. For a real situation, you'd be more likely to pick up the newest file in a directory (with ls -ltr /backups/ | awk '{print $9}' | tail -1, for example) and compare that to the current date.
As with most things in Bash scripting, there are other ways to accomplish this date checking. You could argue that we could leave the extension in the file variable and use a regular expression that parses the date: that would work just as well, with pretty much the same amount of work.
The takeaway from this example (and the whole book, really) should be to use something that works for you and your organization, as long as you've built it in a robust manner and added the necessary comments for everyone to understand what you did!