Shell by Example: File Paths POSIX

Shell provides several tools for manipulating file paths. The key commands are dirname, basename, and realpath (or readlink).

dirname extracts the directory portion:

Edit
#!/bin/sh
path="/home/user/documents/report.txt"
echo "Path: $path"
echo "Directory: $(dirname "$path")"
Output:
Path: /home/user/documents/report.txt
Directory: /home/user/documents

Multiple dirname calls go up the tree:

Edit
#!/bin/sh
path="/home/user/documents/report.txt"
echo "Parent: $(dirname "$(dirname "$path")")"
Output:
Parent: /home/user

basename extracts the filename:

Edit
#!/bin/sh
path="/home/user/documents/report.txt"
echo "Filename: $(basename "$path")"
Output:
Filename: report.txt

basename can strip a suffix:

Edit
#!/bin/sh
path="/home/user/documents/report.txt"
echo "Without .txt: $(basename "$path" .txt)"
Output:
Without .txt: report

Combine dirname and basename:

Edit
#!/bin/sh
fullpath="/var/log/syslog"
dir=$(dirname "$fullpath")
name=$(basename "$fullpath")
echo "Dir: $dir, Name: $name"
Output:
Dir: /var/log, Name: syslog

Handle paths with spaces:

Edit
#!/bin/sh
spacepath="/home/user/my documents/my file.txt"
echo "Path with spaces:"
echo "  Dir: $(dirname "$spacepath")"
echo "  Name: $(basename "$spacepath")"
Output:
Path with spaces:
  Dir: /home/user/my documents
  Name: my file.txt

Parameter expansion alternatives (faster, no subshell):

Edit
#!/bin/sh
file="/path/to/document.tar.gz"
echo "Parameter expansion:"
echo "  Directory: ${file%/*}"   # Remove last /...
echo "  Filename: ${file##*/}"   # Remove through last /
echo "  Extension: ${file##*.}"  # Remove through last .
echo "  Without ext: ${file%.*}" # Remove last .xxx
echo "  Base name: ${file##*/}"
echo "  Base without ext: $(basename "${file%.*}")"
Output:
Parameter expansion:
  Directory: /path/to
  Filename: document.tar.gz
  Extension: gz
  Without ext: /path/to/document.tar
  Base name: document.tar.gz
  Base without ext: document.tar

Get absolute path with realpath (GNU) or readlink:

Edit
#!/bin/sh
echo "Absolute paths:"
# realpath resolves symlinks
realpath . 2>/dev/null || readlink -f . 2>/dev/null || pwd
Output:
Absolute paths:
/tmp

PWD gives current directory:

Edit
#!/bin/sh
echo "Current directory: $PWD"
Output:
Current directory: /tmp

Canonical path (resolve symlinks):

Edit
#!/bin/sh
if command -v realpath >/dev/null 2>&1; then
    echo "Canonical /usr/bin: $(realpath /usr/bin)"
elif command -v readlink >/dev/null 2>&1; then
    echo "Canonical /usr/bin: $(readlink -f /usr/bin 2>/dev/null || echo '/usr/bin')"
fi
Output:
Canonical /usr/bin: /usr/bin

Check if path is absolute:

Edit
#!/bin/sh
is_absolute() {
    case "$1" in
        /*) return 0 ;;
        *) return 1 ;;
    esac
}

is_absolute "/home/user" && echo "/home/user is absolute"
is_absolute "relative/path" || echo "relative/path is relative"
Output:
/home/user is absolute
relative/path is relative

Join paths safely:

Edit
#!/bin/sh
join_path() {
    # Remove trailing slash from first, leading from second
    printf '%s/%s\n' "${1%/}" "${2#/}"
}

echo "Joined: $(join_path "/home/user" "documents")"
echo "Joined: $(join_path "/home/user/" "/documents")"
Output:
Joined: /home/user/documents
Joined: /home/user/documents

Get file extension:

Edit
#!/bin/sh
get_extension() {
    filename="$1"
    case "$filename" in
        *.*) echo "${filename##*.}" ;;
        *) echo "" ;;
    esac
}

echo "Extension of file.txt: $(get_extension "file.txt")"
echo "Extension of archive.tar.gz: $(get_extension "archive.tar.gz")"
echo "Extension of noext: $(get_extension "noext")"
Output:
Extension of file.txt: txt
Extension of archive.tar.gz: gz
Extension of noext: 

Change extension:

Edit
#!/bin/sh
change_extension() {
    echo "${1%.*}.$2"
}

echo "Change ext: $(change_extension "file.txt" "md")"
Output:
Change ext: file.md

Relative path from one location to another via GNU realpath

Edit
#!/bin/sh
mkdir -p /tmp/user/docs
touch /tmp/user/docs/file.txt

realpath --relative-to=/tmp/user /tmp/user/docs/file.txt
Output:
docs/file.txt

Path normalization (remove . and ..):

Edit
#!/bin/sh
normalize_path() {
    # Use cd and pwd for normalization
    cd "$1" 2>/dev/null && pwd
}

# Example if directories exist:
echo "Normalized /tmp/.: $(normalize_path "/tmp/.")"
Output:
Normalized /tmp/.: /tmp

Check common path conditions:

Edit
#!/bin/sh
testpath="/tmp"
echo "Path checks for $testpath:"
[ -e "$testpath" ] && echo "  Exists"
[ -d "$testpath" ] && echo "  Is directory"
[ -f "$testpath" ] && echo "  Is regular file" || echo "  Not regular file"
[ -L "$testpath" ] && echo "  Is symlink" || echo "  Not symlink"
[ -r "$testpath" ] && echo "  Is readable"
[ -w "$testpath" ] && echo "  Is writable"
[ -x "$testpath" ] && echo "  Is executable"
Output:
Path checks for /tmp:
  Exists
  Is directory
  Not regular file
  Not symlink
  Is readable
  Is writable
  Is executable

Script’s own directory:

Edit
#!/bin/sh
echo "Script location:"
echo "  \$0: $0"

# Get script directory (works for sourced scripts too)
script_dir() {
    # Try readlink first for symlinks
    if command -v readlink >/dev/null 2>&1; then
        dir=$(dirname "$(readlink -f "$0" 2>/dev/null || echo "$0")")
    else
        dir=$(dirname "$0")
    fi
    # Make absolute
    cd "$dir" 2>/dev/null && pwd || echo "$dir"
}
Output:
Script location:
  $0: /script

Split path into components:

Edit
#!/bin/sh
echo "Path components:"
path="/usr/local/bin/script"
echo "$path" | tr '/' '\n' | while read -r component; do
    [ -n "$component" ] && echo "  $component"
done
Output:
Path components:
  usr
  local
  bin
  script

Find common prefix of paths:

Edit
#!/bin/sh
common_prefix() {
    printf '%s\n%s\n' "$1" "$2" | sed -e 'N;s/^\(.*\).*\n\1.*$/\1/'
}

echo "Common prefix:"
echo "  $(common_prefix "/home/user/docs" "/home/user/pics")"
Output:
Common prefix:
  /home/user/

« Writing Files | Directories »