[bash] Relative paths based on file location instead of current working directory

Given:

some.txt
dir
 |-cat.sh

With cat.sh having the content:

cat ../some.txt

Then running ./cat.sh inside dir works fine while running ./dir/cat.sh on the same level as dir does not. I expect this to be due to the different working directories. Is there an easy way to make the path ../some.txt relative to the location of cat.sh?

This question is related to bash shell

The answer is


What you want to do is get the absolute path of the script (available via ${BASH_SOURCE[0]}) and then use this to get the parent directory and cd to it at the beginning of the script.

#!/bin/bash
parent_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P )

cd "$parent_path"
cat ../some.text

This will make your shell script work independent of where you invoke it from. Each time you run it, it will be as if you were running ./cat.sh inside dir.

Note that this script only works if you're invoking the script directly (i.e. not via a symlink), otherwise the finding the current location of the script gets a little more tricky)


@Martin Konecny's answer provides the correct answer, but - as he mentions - it only works if the actual script is not invoked through a symlink residing in a different directory.

This answer covers that case: a solution that also works when the script is invoked through a symlink or even a chain of symlinks:


Linux / GNU readlink solution:

If your script needs to run on Linux only or you know that GNU readlink is in the $PATH, use readlink -f, which conveniently resolves a symlink to its ultimate target:

 scriptDir=$(dirname -- "$(readlink -f -- "$BASH_SOURCE")")

Note that GNU readlink has 3 related options for resolving a symlink to its ultimate target's full path: -f (--canonicalize), -e (--canonicalize-existing), and -m (--canonicalize-missing) - see man readlink.
Since the target by definition exists in this scenario, any of the 3 options can be used; I've chosen -f here, because it is the most well-known one.


Multi-(Unix-like-)platform solution (including platforms with a POSIX-only set of utilities):

If your script must run on any platform that:

  • has a readlink utility, but lacks the -f option (in the GNU sense of resolving a symlink to its ultimate target) - e.g., macOS.

    • macOS uses an older version of the BSD implementation of readlink; note that recent versions of FreeBSD/PC-BSD do support -f.
  • does not even have readlink, but has POSIX-compatible utilities - e.g., HP-UX (thanks, @Charles Duffy).

The following solution, inspired by https://stackoverflow.com/a/1116890/45375, defines helper shell function, rreadlink(), which resolves a given symlink to its ultimate target in a loop - this function is in effect a POSIX-compliant implementation of GNU readlink's -e option, which is similar to the -f option, except that the ultimate target must exist.

Note: The function is a bash function, and is POSIX-compliant only in the sense that only POSIX utilities with POSIX-compliant options are used. For a version of this function that is itself written in POSIX-compliant shell code (for /bin/sh), see here.

  • If readlink is available, it is used (without options) - true on most modern platforms.

  • Otherwise, the output from ls -l is parsed, which is the only POSIX-compliant way to determine a symlink's target.
    Caveat: this will break if a filename or path contains the literal substring -> - which is unlikely, however.
    (Note that platforms that lack readlink may still provide other, non-POSIX methods for resolving a symlink; e.g., @Charles Duffy mentions HP-UX's find utility supporting the %l format char. with its -printf primary; in the interest of brevity the function does NOT try to detect such cases.)

  • An installable utility (script) form of the function below (with additional functionality) can be found as rreadlink in the npm registry; on Linux and macOS, install it with [sudo] npm install -g rreadlink; on other platforms (assuming they have bash), follow the manual installation instructions.

If the argument is a symlink, the ultimate target's canonical path is returned; otherwise, the argument's own canonical path is returned.

#!/usr/bin/env bash

# Helper function.
rreadlink() ( # execute function in a *subshell* to localize the effect of `cd`, ...

  local target=$1 fname targetDir readlinkexe=$(command -v readlink) CDPATH= 

  # Since we'll be using `command` below for a predictable execution
  # environment, we make sure that it has its original meaning.
  { \unalias command; \unset -f command; } &>/dev/null

  while :; do # Resolve potential symlinks until the ultimate target is found.
      [[ -L $target || -e $target ]] || { command printf '%s\n' "$FUNCNAME: ERROR: '$target' does not exist." >&2; return 1; }
      command cd "$(command dirname -- "$target")" # Change to target dir; necessary for correct resolution of target path.
      fname=$(command basename -- "$target") # Extract filename.
      [[ $fname == '/' ]] && fname='' # !! curiously, `basename /` returns '/'
      if [[ -L $fname ]]; then
        # Extract [next] target path, which is defined
        # relative to the symlink's own directory.
        if [[ -n $readlinkexe ]]; then # Use `readlink`.
          target=$("$readlinkexe" -- "$fname")
        else # `readlink` utility not available.
          # Parse `ls -l` output, which, unfortunately, is the only POSIX-compliant 
          # way to determine a symlink's target. Hypothetically, this can break with
          # filenames containig literal ' -> ' and embedded newlines.
          target=$(command ls -l -- "$fname")
          target=${target#* -> }
        fi
        continue # Resolve [next] symlink target.
      fi
      break # Ultimate target reached.
  done
  targetDir=$(command pwd -P) # Get canonical dir. path
  # Output the ultimate target's canonical path.
  # Note that we manually resolve paths ending in /. and /.. to make sure we
  # have a normalized path.
  if [[ $fname == '.' ]]; then
    command printf '%s\n' "${targetDir%/}"
  elif  [[ $fname == '..' ]]; then
    # Caveat: something like /var/.. will resolve to /private (assuming
    # /var@ -> /private/var), i.e. the '..' is applied AFTER canonicalization.
    command printf '%s\n' "$(command dirname -- "${targetDir}")"
  else
    command printf '%s\n' "${targetDir%/}/$fname"
  fi
)

# Determine ultimate script dir. using the helper function.
# Note that the helper function returns a canonical path.
scriptDir=$(dirname -- "$(rreadlink "$BASH_SOURCE")")

Just one line will be OK.

cat "`dirname $0`"/../some.txt