basename and dirname Explained: Splitting Paths in Scripts
Scripts constantly need to take a path apart: the filename without its directory, or the directory without the filename. basename and dirname do exactly that, and they get the edge cases right.
The two commands
basename /var/log/nginx/access.log
# access.log
dirname /var/log/nginx/access.log
# /var/log/nginx
Stripping an extension:
basename /path/to/archive.tar.gz .tar.gz
# archive
basename -s .log /var/log/syslog.log
# syslog
The -s form is easier to read and works with multiple arguments:
basename -s .txt file1.txt file2.txt file3.txt
The classic use
Finding the directory a script lives in, so it can reference files next to itself regardless of where it was invoked from:
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib/common.sh"
That idiom appears in most well-written shell scripts. dirname gets the directory, cd and pwd turn it into an absolute path that survives symlinks in the invocation.
Renaming in a loop
for f in *.jpeg; do
mv -- "$f" "$(basename -- "$f" .jpeg).jpg"
done
The -- matters. It stops a filename beginning with a dash being interpreted as an option, which is the sort of thing that never happens until it does.
Shell alternatives
Bash can do both with parameter expansion, without launching a process:
path=/var/log/nginx/access.log
echo "${path##*/}" # access.log (basename)
echo "${path%/*}" # /var/log/nginx (dirname)
echo "${path%.*}" # strip last extension
echo "${path##*.}" # just the extension
Inside a loop over thousands of files this is meaningfully faster, because each basename call forks a process.
The behaviour differs on edge cases though. ${path%/*} on a bare filename returns the filename unchanged rather than a dot, which is not what dirname does. For a script processing arbitrary input, the commands are safer; for a loop over known-shaped paths, the expansions are faster.
Our bash arrays and parameter expansion guide covers the syntax in full.
Edge cases worth knowing
dirname file.txt # . not empty
dirname / # /
basename / # /
basename /path/to/dir/ # dir trailing slash ignored
dirname /path/to/dir/ # /path/to
dirname returning . for a bare filename is correct and catches people out, because concatenating it produces ./file.txt rather than file.txt.
When you want realpath instead
These two are pure string manipulation. They never look at the filesystem, which means they work on paths that do not exist and they do not resolve symlinks or .. components.
dirname /var/log/../log/syslog
# /var/log/../log unchanged
When you need the real canonical location, use realpath.
Frequently Asked Questions
What do basename and dirname do?
basename strips the directory part of a path and returns the final component. dirname does the opposite, returning everything before the final component. Together they split a path into the two halves scripts usually need.
How do I strip a file extension?
Pass the extension as a second argument to basename, or use its -s flag. Both remove that suffix from the result, which saves a separate sed or parameter expansion step.
Are the shell built-in alternatives better?
They are faster because they avoid launching a process, which matters inside a loop over thousands of files. They also behave slightly differently on edge cases like trailing slashes, so they are not a drop-in replacement in every case.
What does dirname return for a bare filename?
A single dot, meaning the current directory. That is correct and it surprises people who expected an empty string, and it is why concatenating the result blindly can produce paths beginning with ./ unexpectedly.
How do these handle filenames with spaces?
Correctly, as long as you quote the argument and quote the result. The commands themselves are fine; the bugs come from unquoted expansions in the surrounding script.
What is the difference between dirname and realpath?
dirname performs pure string manipulation and never touches the filesystem, so it works on paths that do not exist. realpath resolves symlinks and relative components against the real filesystem and fails if the path is not there.