Relative paths based on file location instead of current working directory [duplicate]
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
?
Solution 1:
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)
Solution 2:
@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
.
- macOS uses an older version of the BSD implementation of
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 lackreadlink
may still provide other, non-POSIX methods for resolving a symlink; e.g., @Charles Duffy mentions HP-UX'sfind
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 havebash
), 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")")
Solution 3:
Just one line will be OK.
cat "`dirname $0`"/../some.txt