Normalizing Path Names with Bash
The bash function presented here normalizes path names. By normalize I mean it removes unneeded /./ and ../dir sequences. For example, ../d1/./d2/../f1 normalized would be ../d1/f1.
The first version of the function uses bash regular expressions. The /./ sequences are removed first during variable expansion with substitution by the line:
local path=${1//\/.\//\/}
The dir/.. sequences are removed by the loop:
while [[ $path =~ ([^/][^/]*/\.\./) ]]
do
path=${path/${BASH_REMATCH[0]}/}
done
Each time a dir/.. match is found, variable expansion with substitution is used to remove the matched part of the path.
Regular expressions were introduced in bash 3.0. Bash 3.2 changed regular expression handling slightly in that quotes around regular expressions became part of the regular expression. So, if you have a version of bash (with regular expression support) and the code doesn't work, put the regular expression in the while loop in quotes.
The entire function and some test code follows:
#!/bin/bash
#
# Usage: normalize_path PATH
#
# Remove /./ and dir/.. sequences from a pathname and write result to stdout.
function normalize_path()
{
# Remove all /./ sequences.
local path=${1//\/.\//\/}
# Remove dir/.. sequences.
while [[ $path =~ ([^/][^/]*/\.\./) ]]
do
path=${path/${BASH_REMATCH[0]}/}
done
echo $path
}
if [[ $(basename $0 .sh) == 'normalize_path' ]]; then
if [[ "$*" ]]; then
for p in "$@"
do
printf "%-30s => %s\n" $p $(normalize_path $p)
done
else
for p in /test/../test/file test/../test/file .././test/../test/file
do
printf "%-30s => %s\n" $p $(normalize_path $p)
done
fi
fi
#####################################################################
# vim: tabstop=4: shiftwidth=4: noexpandtab:
# kate: tab-width 4; indent-width 4; replace-tabs false;
Since, older versions of bash don't support regular expressions the second version does the same thing using sed instead:
#!/bin/bash
#
# Usage: normalize_path PATH
#
# Remove /./ and dir/.. sequences from a pathname and write result to stdout.
function normalize_path()
{
# Remove all /./ sequences.
local path=${1//\/.\//\/}
# Remove first dir/.. sequence.
local npath=$(echo $path | sed -e 's;[^/][^/]*/\.\./;;')
# Remove remaining dir/.. sequence.
while [[ $npath != $path ]]
do
path=$npath
npath=$(echo $path | sed -e 's;[^/][^/]*/\.\./;;')
done
echo $path
}
if [[ $(basename $(basename $0 .sh) .old) == 'normalize_path' ]]; then
if [[ "$*" ]]; then
for p in "$@"
do
printf "%-30s => %s\n" $p $(normalize_path $p)
done
else
for p in /test/../test/file test/../test/file .././test/../test/file
do
printf "%-30s => %s\n" $p $(normalize_path $p)
done
fi
fi
#####################################################################
# vim: tabstop=4: shiftwidth=4: noexpandtab:
# kate: tab-width 4; indent-width 4; replace-tabs false;
You can run the script directly and it runs a few tests:
$ bash normalize_path.sh
/test/../test/file => /test/file
test/../test/file => test/file
.././test/../test/file => ../test/file
You can also pass in test cases on the command line:
$ bash normalize_path.sh ../d1/./d2/../f1 a/b/c/../d/../e
../d1/./d2/../f1 => ../d1/f1
a/b/c/../d/../e => a/b/e
Normalized path names are never necessary but they're often easier to comprehend at a glance.