Skip to content

Instantly share code, notes, and snippets.

@coltrane
Last active September 3, 2025 06:19
Show Gist options
  • Save coltrane/3467fd5231e74c6d1fe9c8f1f4b3930d to your computer and use it in GitHub Desktop.
Save coltrane/3467fd5231e74c6d1fe9c8f1f4b3930d to your computer and use it in GitHub Desktop.
Shell script that normalizes a path without requiring the path to actually exist on the filesystem. Works in sh, bash, and zsh.
#!/usr/bin/env sh
#
# Removes all '.', '..', and '//' segments, creating a canonical
# form of the path string. The path does not have to actually exist
# in the filesystem.
#
normalize_path() {
if [[ "$1" == "" ]] ; then
echo "$1"
return
fi
parts=()
if [ -n "${ZSH_VERSION:-}" ] ; then
IFS="/" read -r -A parts <<< "$1"
else
IFS="/" read -r -a parts <<< "$1"
parts=("" "${parts[@]}")
fi
length=${#parts[@]}
skip=0
out=""
abs=""
for ((i=length; i>0; i--)); do
s=${parts[i]:-}
if [[ "$s" == "." ]] ; then
# if (( i == 0 )) ; then out=".${out:+/$out}"; fi
continue
fi
if [[ "$s" == "" ]] ; then
if (( i == 1 )) ; then abs="/" ; fi
continue;
fi
if [[ "$s" == ".." ]] ; then
skip=$((skip + 1))
continue
fi
if (( skip > 0 )) ; then
skip=$((skip - 1))
continue
fi
out="${parts[i]}${out:+"/$out"}"
done
if [[ ! ($abs || "$out" == "." || $out == ./*) ]] ; then
# add leading '..' segments only if it makes sense to do so
for ((i=skip; i>0; i--)); do
out="..${out:+"/$out"}"
done
fi
if [[ $abs ]] ; then
# output is absolute if input was absolute
out="/${out}"
fi
if [[ "$1" == */ ]] ; then
# use a trailing slash if the input had one
out="${out%/}/"
fi
echo "$out"
}
normalize_path "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment