You need the directory part of a path in a script, and ${var%/*} gets it wrong the moment there is a trailing slash. You will learn a repeatable way to strip the last component from a path with dirname, process several names in one call, and pick newline or NUL output for the next command in a pipeline. It takes about ten minutes. The examples use GNU dirname from coreutils 9.4, installed here as package version 9.4-3ubuntu6.3.
Confirm which executable your shell will run, then read its local help:
$ command -v dirname
/usr/bin/dirname
$ dirname --version | sed -n '1p'
dirname (GNU coreutils) 9.4
$ dirname --help | sed -n '1,12p'
Usage: dirname [OPTION] NAME...
Output each NAME with its last non-slash component and trailing slashes
removed; if NAME contains no /'s, output '.' (meaning the current directory).
-z, --zero end each output line with NUL, not newline
--help display this help and exit
--version output version information and exit
The command takes one or more NAME arguments. It removes the final non-slash component and any trailing slashes, then prints what is left. If a name has no slash, the documented result is ., meaning the current directory.
Checkpoint: If dirname --version reports a different implementation or version, keep the local help beside you. Option details and diagnostics can vary between implementations.
Pass the path as a single shell argument:
$ dirname /srv/www/site/index.html
/srv/www/site
This is a textual transformation. The path does not have to name an existing file, which makes it handy for building a destination, log name or configuration value. Check the result directly when the parent matters:
$ parent=$(dirname /srv/www/site/index.html)
$ printf 'parent=%s\n' "$parent"
parent=/srv/www/site
Quote the result when you use it later. The quotes keep a path containing whitespace as one argument:
$ input='/srv/Customer Files/report.csv'
$ parent=$(dirname "$input")
$ printf '%s\n' "$parent"
/srv/Customer Files
Tip: The printed parent is not a directory change. dirname never changes the shell's working directory. To work there, validate it with a separate command such as test -d, then use cd -- "$parent" on purpose.
Trailing slashes are removed before the final component is selected:
$ dirname /usr/bin/
/usr
$ dirname foo/
.
$ dirname foo//bar///
foo
A name with no slash gives a dot, not an empty string:
$ dirname stdio.h
.
$ dirname report.csv
.
The root path stays the root path:
$ dirname /
/
$ dirname //
/
These results are easy to misread inside a bigger script:
. means the current directory. It does not mean the input was invalid./ is a real root path. Do not add another slash or treat it as a blank parent.Checkpoint: Test your script's real input shapes with harmless names before you use the result to build a file operation. Include a bare filename, a path ending in /, and a path whose parent is /.
Multiple names give one output line per input, in the same order:
$ dirname /var/log/app/error.log /home/alice/notes/today.txt report.csv
/var/log/app
/home/alice/notes
.
That suits a short, controlled list of paths. Keep the arguments quoted if they come from shell variables:
$ first='/var/log/app/error.log'
$ second='/home/alice/notes/today.txt'
$ dirname "$first" "$second"
/var/log/app
/home/alice/notes
Warning: Do not use unquoted variables for arbitrary names. Word splitting can turn one path containing spaces into several arguments, and pathname expansion can swap wildcard characters for unrelated directory entries before dirname sees them.
The default separator is a newline. That reads well in a terminal, but a newline can also be part of a Unix filename. GNU dirname has -z or --zero to end each result with a NUL byte instead:
$ dirname --zero /srv/app/log.txt report.csv | od -An -t x1
2f 73 72 76 2f 61 70 70 00 2e 00
The hexadecimal 00 bytes are the separators. Three rules follow:
--zero does not do. It changes only the terminator. It does not quote or escape the path text.For ordinary, trusted path lists, newline output is simpler. Reach for --zero when the next tool supports NUL records and the input may contain unusual characters.
Run the command with no arguments to see how missing input is reported, and capture the status straight away:
$ dirname
dirname: missing operand
Try 'dirname --help' for more information.
$ printf 'exit status: %s\n' "$?"
exit status: 1
The diagnostic wording can be translated or change between releases. What you can rely on is that the command writes an error and returns non-zero. Never let a failed command feed an empty or stale parent into a later file operation.
A path that does not exist is not an error for dirname:
$ dirname /path/that/does/not/exist/output.txt
/path/that/does/not/exist
If your workflow needs the parent to exist, check that separately. This read-only test stops before a later operation if the computed parent is absent:
parent=$(dirname -- "$input") || exit 1
if test ! -d "$parent"; then
printf 'parent directory is missing: %s\n' "$parent" >&2
exit 1
fi
The check creates nothing and needs no sudo.
Warning: If a later command will write, delete or move files, review that command separately before you run it. Those operations can change state even though dirname itself cannot.
. and a root path returns /.--zero only with a consumer that understands NUL-delimited records.dirname's exit status before using its result in a state-changing command.