Both have their quirks, unfortunately.
Both are required by POSIX, so the difference between them isn't a portability concern¹.
The plain way to use the utilities is
base=$(basename -- "$filename")
dir=$(dirname -- "$filename")
Note the double quotes around variable substitutions, as always, and also the --
after the command, in case the file name begins with a dash (otherwise the commands would interpret the file name as an option). This still fails in one edge case, which is rare but might be forced by a malicious user²: command substitution removes trailing newlines. So if a filename is called foo/bar
then base
will be set to bar
instead of bar
. A workaround is to add a non-newline character and strip it after the command substitution:
base=$(basename -- "$filename"; echo .); base=${base%.}
dir=$(dirname -- "$filename"; echo .); dir=${dir%.}
With parameter substitution, you don't run into edge cases related to expansion of weird characters, but there are a number of difficulties with the slash character. One thing that is not an edge case at all is that computing the directory part requires different code for the case where there is no /
.
base="${filename##*/}"
case "$filename" in
*/*) dirname="${filename%/*}";;
*) dirname=".";;
esac
The edge case is when there's a trailing slash (including the case of the root directory, which is all slashes). The basename
and dirname
commands strip off trailing slashes before they do their job. There's no way to strip the trailing slashes in one go if you stick to POSIX constructs, but you can do it in two steps. You need to take care of the case when the input consists of nothing but slashes.
case "$filename" in
*/*[!/]*)
trail=${filename##*[!/]}; filename=${filename%%"$trail"}
base=${filename##*/}
dir=${filename%/*};;
*[!/]*)
trail=${filename##*[!/]}
base=${filename%%"$trail"}
dir=".";;
*) base="/"; dir="/";;
esac
If you happen to know that you aren't in an edge case (e.g. a find
result other than the starting point always contains a directory part and has no trailing /
) then parameter expansion string manipulation is straightforward. If you need to cope with all the edge cases, the utilities are easier to use (but slower).
Sometimes, you may want to treat foo/
like foo/.
rather than like foo
. If you're acting on a directory entry then foo/
is supposed to be equivalent to foo/.
, not foo
; this makes a difference when foo
is a symbolic link to a directory: foo
means the symbolic link, foo/
means the target directory. In that case, the basename of a path with a trailing slash is advantageously .
, and the path can be its own dirname.
case "$filename" in
*/) base="."; dir="$filename";;
*/*) base="${filename##*/}"; dir="${filename%"$base"}";;
*) base="$filename"; dir=".";;
esac
The fast and reliable method is to use zsh with its history modifiers (this first strips trailing slashes, like the utilities):
dir=$filename:h base=$filename:t
¹ Unless you're using pre-POSIX shells like Solaris 10 and older's /bin/sh
(which lacked parameter expansion string manipulation features on machines still in production — but there's always a POSIX shell called sh
in the installation, only it's /usr/xpg4/bin/sh
, not /bin/sh
).
² For example: submit a file called foo
to a file upload service that doesn't protect against this, then delete it and cause foo
to be deleted instead
base=$(basename -- "$filename"; echo .); base=${base%.}; dir=$(dirname -- "$filename"; echo .); dir=${dir%.}
? I was reading carefully and I didn't notice you mentioning any drawbacks. – Wildcard Jan 12 '16 at 22:10foo/
likefoo
, not likefoo/.
, which isn't consistent with POSIX-compliant utilities. – Gilles 'SO- stop being evil' Jan 12 '16 at 22:11/
if I need it. – Wildcard Jan 12 '16 at 22:17find
result, which always contains a directory part and has no trailing/
" Not quite true,find ./
will output./
as the first result. – Tavian Barnes Mar 19 '19 at 04:10${p%"${p##*[!/]}"}
– Stéphane Chazelas Jun 26 '20 at 05:51basename
anddirname
commands. Routers or phones, mostly. – mtraceur Jun 17 '21 at 06:23