Sample output
$ p=/var/log/nginx/access.log.2.gz; echo "${p##*/} ${p%/*} ${p%.*} ${p##*.}"
access.log.2.gz /var/log/nginx /var/log/nginx/access.log.2 gz
Four answers from one string, and no second process involved. The pattern after each operator is a glob, not a regex, and the operator decides which end it is matched against and whether it is greedy: # for the front, % for the end, doubled for the longest match.
When you would reach for it
Inside a loop over files, where $(basename "$f") forks a process for every iteration and a thousand files means a thousand forks. It is also the tool for building one filename out of another: the .bak next to the original, the .txt that replaces the .md, the output directory that mirrors the input. Anything you would reach for sed to do to a single variable, one of these four operators probably does without leaving the shell.
Gotchas
- These are not exact copies of
basenameanddirname. With a trailing slash,p=/tmp/dir/gives an empty${p##*/}wherebasenameprintsdir. With no slash at all,p=notes.txtgivesnotes.txtback from${p%/*}wheredirnameprints.. If paths arrive from users or from other tools, strip the trailing slash first with${p%/}, and check whether the directory result still contains a slash before trusting it. - A missing extension does not produce an empty string.
p=MakefilegivesMakefilefrom${p##*.}because the pattern never matched, so nothing was removed. Test for it with[ "${p##*.}" = "$p" ], or check that the name contains a dot before you ask for the extension. - Dotfiles break the extension logic.
p=.bashrcgives an empty string from${p%.*}, since the whole name counts as an extension, and running the counting variant below over a tree with.hidden-configin it reportshidden-configas a file type. Filter dotfiles out first if the answer matters. - A pattern stored in a variable is still a pattern. With
ext='*.gz',${p%$ext}treats the asterisk as a wildcard, and${p%"$ext"}quotes it into a literal. The quotes go inside the braces, around the pattern alone.
Variants
$ for f in docs/*.md; do echo mv -- "$f" "${f%.md}.txt"; done
A dry run for a bulk rename, printing each mv rather than running it. Read the list, then remove the echo to do it for real, and check before you run it: mv overwrites an existing .txt without asking
$ find . -type f | while IFS= read -r f; do echo "${f##*.}"; done | sort | uniq -c | sort -rn
Count the file types in a tree by extension. Files with no dot show up as their full path, which is a quick way to spot them
$ f="Q3 sales report (final).csv"; echo "${f// /_}"
Replace every space with an underscore. The ${var//old/new} substitution form is bash, ksh, and zsh only, and dash rejects it with “Bad substitution”, so keep it out of #!/bin/sh scripts