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 )" ```

Original source

Related problems