bashposixsh

How to get script directory in POSIX sh?


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

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:

    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.

    Note that an enhanced version of this function is available as a downloadable utility, rreadlink; use it with the -e option. If you have Node.js installed, you can install it with [sudo] npm install rreadlink -g


    How to handle sourced invocations:

    tl;dr:

    Using POSIX features only:

    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 )
    

    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:

    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.

    For the sake of completeness: the value of $0 in other contexts: