How to get script directory in POSIX sh?
bash, posix, sh
Solution
The POSIX-shell (`sh`) counterpart of `$BASH_SOURCE` is `$0`. see bottom for background info
Caveat: The crucial difference is that if your script is being sourced (loaded into the current shell with `.`), the snippets below will not work properly. explanation further below
Note that I've changed `DIR` to `dir` in the snippets below, because it's better not to use all-uppercase variable names so as to avoid clashes with environment variables and special shell variables. The `CDPATH= ` prefix takes the place of `> /dev/null` in the original command: `$CDPATH` is set to a null string so as to ensure that `cd` never echoes anything.
In the simplest case, this will do (the equivalent of the OP's command):
dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
If you also want to resolve the resulting directory path to its ultimate target in case the directory and/or its components are symlinks, add `-P` to the `pwd` command:
dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)
Caveat: This is NOT the same as finding the script's own true directory of origin: Let's say your script `foo` is symlinked to `/usr/local/bin/foo` in the `$PATH`, but its true path is `/foodir/bin/foo`. The above will still report `/usr/local/bin`, because the symlink resolution (`-P`) is applied to the directory, `/usr/local/bin`, rather than to the script itself.
To find the script's own true directory of origin, you'd have to inspect the script's path to see if it's a symlink and, if so, follow the (chain of) symlinks to the ultimate target file, and then extract the directory path from the target file's canonical path.
GNU's `readlink -f` (better: `readlink -e`) could do that for you, but `readlink` is not a POSIX utility. While BSD platforms, including macOS, have a `readlink` utility too, on macOS it doesn't support `-f`'s functionality. That said, to show how simple the task becomes if `readlink -f` is available: `dir=$(dirname "$(readlink -f -- "$0")")`.
In fact, there is no POSIX utility for resolving file symlinks. There are ways to work around that, but they're cumbersome and not fully robust:
The following, POSIX-compliant shell function implements what GNU's `readlink -e` does and is a reasonably robust solution that only fails in two rare edge cases:
- paths with embedded newlines (very rare)
- filenames containing literal string `->` (also rare)
With this function, named `rreadlink`, defined, the following determines the script's true directory path of origin:
dir=$(dirname -- "$(rreadlink "$0")")
Note: If you're willing to assume the presence of a (non-POSIX) `readlink` utility - which would cover macOS, FreeBSD and Linux - a similar, but simpler solution can be found in this answer to a related question.
`rreadlink()` source code - place before calls to it in scripts:
rreadlink() ( # Execute the function in a *subshell* to localize variables and the effect of `cd`.
target=$1 fname= targetDir= CDPATH=
# Try to make the execution environment as predictable as possible:
# All commands below are invoked via `command`, so we must make sure that `command`
# itself is not redefined as an alias or shell function.
# (Note that command is too inconsistent across shells, so we don't use it.)
# `command` is a *builtin* in bash, dash, ksh, zsh, and some platforms do not even have
# an external utility version of it (e.g, Ubuntu).
# `command` bypasses aliases and shell functions and also finds builtins
# in bash, dash, and ksh. In zsh, option POSIX_BUILTINS must be turned on for that
# to happen.
{ \unalias command; \unset -f command; } >/dev/null 2>&1
[ -n "$ZSH_VERSION" ] && options[POSIX_BUILTINS]=on # make zsh find *builtins* with `command` too.
while :; do # Resolve potential symlinks until the ultimate target is found.
[ -L "$target" ] || [ -e "$target" ] || { command printf '%s\n' "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 may be defined
# *relative* to the symlink's own directory.
# Note: We parse `ls -l` output to find the symlink target
# which is the only POSIX-compliant, albeit somewhat fragile, way.
target=$(command ls -l "$fname")
target=${target#* -> }
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
)
To be robust and predictable, the function uses `command` to ensure that only shell builtins or external utilities are called (ignores overloads in the forms of aliases and functions). It's been tested in recent versions of the following shells: `bash`, `dash`, `ksh`, `zsh`.
How to handle sourced invocations:
tl;dr:
Using POSIX features only:
You cannot determine the script's path in a sourced invocation (except in `zsh`, which, however, doesn't usually act as `sh`).
You can detect whether or not your script is being sourced ONLY if your script is being sourced directly by the shell (such as in a shell profile/initialization file; possibly via a chain of sourcings), by comparing `$0` to the shell executable name/path (except in `zsh`, where, as noted `$0` is truly the current script's path). By contrast (except in `zsh`), a script being sourced from another script that itself was directly invoked, contains that script's path in `$0`.
To solve these problems, `bash`, `ksh`, and `zsh` have nonstandard features that do allow determining the actual script path even in sourced scenarios and also detecting whether a script is being sourced or not; for instance, in `bash`, `$BASH_SOURCE` always contains the running script's path, whether it's being sourced or not, and `[[ $0 != "$BASH_SOURCE" ]]` can be used to test whether the script is being sourced.
- This answer indirectly shows these techniques, in the context of determining whether a given script is being sourced.
To show why this cannot be done, let's analyze the command from Walter A's answer:
# NOT recommended - see discussion below.
DIR=$( cd -P -- "$(dirname -- "$(command -v -- "$0")")" && pwd -P )
- (Two asides:
- Using `-P` twice is redundant - it's sufficient to use it with `pwd`.
- The command is missing silencing of `cd`'s potential stdout output, if `$CDPATH` happens to be set.)
- `command -v -- "$0"`
- `command -v -- "$0"` is designed to cover one additional scenario: if the script is being sourced from an interactive shell, `$0` typically contains the mere filename of the shell executable (`sh`), in which case `dirname` would simply return `.` (because that's what `dirname` invariably does when given a argument without a path component). `command -v -- "$0"` then returns that shell's absolute path through a `$PATH` lookup (`/bin/sh`). Note, however, that login shells on some platforms (e.g., OSX) have their filename prefixed with `-` in `$0` (`-sh`), in which case `command -v -- "$0"` doesn't work as intended (returns an empty string).
- Conversely, `command -v -- "$0"` can misbehave in two non-sourced scenarios in which the shell executable, `sh`, is directly invoked, with the script as an argument:
- if the script itself is not executable: `command -v -- "$0"` may return an empty string, depending on what specific shell acts as `sh` on a given system: `bash`, `ksh`, and `zsh` return an empty string; only `dash` echoes `$0` The POSIX spec. for `command` doesn't explicitly say whether `command -v`, when applied to a filesystem path, should only return executable files - which is what `bash`, `ksh`, and `zsh` do - but you can argue that it is implied by the very purpose of `command`; curiously, `dash`, which is usually the most compliant POSIX citizen, is deviating from the standard here. By contrast, `ksh` is the lone model citizen here, because it is the only one that reports executable files only and reports them with an absolute (albeit not normalized) path, as the spec requires.
- if the script is executable, but not in the `$PATH`, and the invocation uses its mere filename (e.g., `sh myScript`), `command -v -- "$0"` will also return the empty string, except in `dash`.
- Given that the script's directory cannot be determined when the script is being sourced - because `$0` then doesn't contain that information (except in `zsh`, which doesn't usually act as `sh`) - there's no good solution to this problem.
- Returning the shell executable's directory path in that situation is of limited use - it is, after all, not the script's directory - except perhaps to later use that path in a test to determine whether or not the script is being sourced.
- A more reliable approach would be to simply test `$0` directly: `[ "$0" = "sh" ] || [ "$0" = "-sh" ] || [ "$0" = "/bin/sh" ]`
- However, even that doesn't work if the script is being sourced from another script (that was itself directly invoked), because `$0` then simply contains the sourcing script's path.
- Given the limited usefulness of `command -v -- "$0"` in sourced scenarios and the fact that it breaks two non-sourced scenarios, my vote is for NOT using it, which leaves us with:
- All non-sourced scenarios are covered.
- In sourced invocations, you cannot determine the script's path, and at best, in limited circumstances, you can detect whether or not sourcing is occurring:
- When sourced directly by the shell (such as from a shell profile/initialization file), `$dir` ends up either containing `.`, if the shell executable was invoked as a mere filename (applying `dirname` to a mere filename always returns `.`), or the shell executable's directory path otherwise. `.` cannot be reliably distinguished from a non-sourced invocation from the current directory.
- When sourced from another script (that was itself not also sourced), `$0` contains that script's path, and the script being sourced has no way of telling whether that's the case.
Background information:
POSIX defines the behavior of `$0` with respect to shell scripts here.
Essentially, `$0` should reflect the path of the script file as specified, which implies:
Do NOT rely on `$0` containing an absolute path.
`$0` contains an absolute path only if:
you explicitly specify an absolute path; e.g.:
- `~/bin/myScript` (assuming the script itself is executable)
- `sh ~/bin/myScript`
you invoke an executable script by mere filename, which requires that it both be executable and in the `$PATH`; behind the scenes, the system transforms `myScript` into an absolute path and then executes it; e.g.:
- `myScript # executes /home/jdoe/bin/myScript, for instance`
In all other cases, `$0` will reflect the script path as specified:
- When explicitly invoking `sh` with a script, this can be a mere filename (e.g., `sh myScript`) or a relative path (e.g., `sh ./myScript`)
- When invoking an executable script directly, this can be a relative path (e.g., `./myScript` - note that a mere filename would only find scripts in the `$PATH`).
In practice, `bash`, `dash`, `ksh`, and `zsh` all exhibit this behavior.
By contrast, POSIX does NOT mandate the value of `$0` when sourcing a script (using the special built-in utility `.` ("dot")), so you cannot rely on it, and, in practice, behavior differs across shells.
- Thus, you cannot blindly use `$0` when your script is being sourced and expect standardized behavior.
- In practice, `bash`, `dash`, and `ksh` leave `$0` untouched when sourcing scripts, meaning that `$0` contains the caller's `$0` value, or, more accurately, the `$0` value of the most recent caller in the call chain that hasn't been sourced itself; thus, `$0` may point either to the shell's executable or to the path of another (directly invoked) script that sourced the current one.
- By contrast, `zsh`, as the lone dissenter, actually does report the current script's path in `$0`. Conversely, `$0` will provide no indication as to whether the script is being sourced or not.
- In short: using POSIX features only, you can neither tell reliably whether the script at hand is being sourced, nor what the script at hand's path is, nor what the relationship of `$0` to the current script's path is.
- If you do need to handle this situation, you must identify the specific shell at hand and access its specific non-standard features:
- `bash`, `ksh`, and `zsh` all offer their own ways of obtaining the running script's path, even when it's being sourced.
For the sake of completeness: the value of `$0` in other contexts:
- Inside a shell function, POSIX mandates that `$0` remain unchanged; therefore, whatever value it has outside the function, it'll have inside as well.
- In practice, `bash`, `dash`, and `ksh` do behave that way.
- Again, `zsh` is the lone dissenter and reports the function's name.
- In a shell that accepted a command string via the `-c` option on startup, it's the first operand (non-option argument) that sets `$0`; e.g.:
- `sh -c 'echo \$0: $0 \$1: $1' foo one # -> '$0: foo $1: one'`
- `bash`, `dash`, `ksh`, and `zsh` all behave that way.
- Otherwise, in a shell not executing a script file, `$0` is the value of the first argument that the shell's parent process passed - typically, that's the shell's name or path (e.g. `sh`, or `/bin/sh`); this includes:
- an interactive shell
- Caveat: some platforms, notably OSX, always create login shells when creating interactive shells, and prepend `-` to the shell name before placing it in `$0`, so as to signal to the shell that it is a _login shell; thus, by default, `$0` reports `-bash`, not `bash`, in interactive shells on OSX.
- a shell that reads commands from stdin
- this also applies to piping a script file to the shell via stdin (e.g., `sh < myScript`)
- `bash`, `dash`, `ksh`, and `zsh` all behave that way.
Problem
I have the following code in my bash script. Now I wanna use it in POSIX sh. How can I convert it? ``` DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" > /dev/null && pwd )" ```