How to get script directory in POSIX sh?
Asked Answered
G

4

94

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 )"
Gallows answered 23/4, 2015 at 18:54 Comment(16)
The problem there is not the array. The problem there is BASH_SOURCE which is a bash-ism. There are other answers in this question and there's the mywiki.wooledge.org/BashFAQ/028Aldo
Your problem isn't so much with arrays as with the BASH_SOURCE variable, which, obviously, is available only in bash.Cockalorum
@EtanReisner, thanks. I have updated my question. do you know what is the substitution of BASH_SOURCE in posix sh? thanksGallows
Try DIR=$( cd -P -- "$(dirname -- "$(command -v -- "$0")")" && pwd -P ), what I found at http://stackoverflow.com/questions/760110/can-i-get-the-absolute-path-to-the-current-script-in-kornshell.Ratline
I wonder what that command usage is supposed to be doing there. That seems like a mistake for anything not in the PATH.Aldo
@EtanReisner: The command -v will work as long as its argument is a path to an executable file (as opposed to a mere filename, in which case matches would be limited to the $PATH). However, it merely echoes that path as is, and while it seems to do no harm, I have no idea why it's there.Suwannee
@Suwannee Do you have a reference for that behavior of command? I see the spec saying "command_names including a <slash> character ... shall be written as absolute pathnames" but that doesn't say anything about the executable part.Aldo
@EtanReisner: I think it's implied by this passage from the POSIX spec. of command (emphasis mine): "When the -v or -V option is used, the command utility shall provide information concerning how a command name is interpreted by the shell."Suwannee
@Suwannee I went looking for a concrete definition of command_name but couldn't find one (I didn't look for command name) but maybe that's it.Aldo
possible duplicate of Bash script: set current working directory to the directory of the scriptBiforate
See also: Reliable way for a bash script to get the full path to itself?Biforate
And: Can a Bash script tell what directory it's stored in?Biforate
@kenorb: Note that this question is explicitly about the POSIX shell (sh), not bash, which is what the question you link to are tagged with; even though the answers there happen to contain solutions that are also POSIX-compliant, they aren't discussed as such.Suwannee
@EtanReisner: command_name is defined as "The name of a utility or a special built-in utility" - I guess any executable file qualifies as a utility. The page also states, "command_names including a <slash> character … shall be written as absolute pathnames" - note the absolute, which only ksh implements that way - dash, bash, and zsh simply echo the path as specified. As an additional curiosity, dash also echoes executable directories. I think I finally figured out the purpose of command -v -- "$0" - it's to - sort of - cover sourced invocation; see my updated answer.Suwannee
@EtanReisner: Addendum: dash actually echoes ANY existing path passed to command -v - even directories - being executable is not a requirement - unlike in bash, ksh, and zsh.Suwannee
CURRENT_SCRIPT_DIRECTORY="$(basename "`pwd "$(dirname -- readlink $0)"`")"Kerr
S
177

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.
Suwannee answered 23/4, 2015 at 22:26 Comment(7)
@Sandburg: I've added a link to the answer you mentioned to this answer, with a caveat that it relies on the non-POSIX readlink utility; given that macOS, FreeBSD and Linux all have this utility, it is a pragmatic and simpler alternative that may work for many.Suwannee
thanks for the rreadlink() function, very helpful! This line: command cd "$(command dirname -- "$target")" located inside the while loop, emits some unexpected output on macOS. I fixed it for my situation by redirecting output to /dev/null: command cd "$(command dirname -- "$target")" >/dev/null 2>&1Tsana
Incredible script for rreadlink(), I am linking to this answer in the script I'm using this in. You have done a fantastic service for the worldwide POSIX community.Conversational
I'm glad to hear it, @cosmicexplorer; I appreciate the nice feedback.Suwannee
@xmnboy, looks like I never responded to your comment: thanks for the hint, but I don't see this problem (as of macOS 11.5.1, where GNU bash, version 3.2.57(1) acts as /bin/sh) - what unexpected output, specifically, are you getting?Suwannee
@Suwannee -- sorry for the delayed response. I just tried to reproduce the issue I encountered on bash 3.x and 5.x (on macOS), as well as on relatively current releases of the dash, ksh and zsh shells, but was not successful. As an aside, I do recall running into a different issue with ksh but that turned out to be a problem with the ksh implementation, not your script. Regardless, thank you for this script, it has been extremely valuable for building POSIX-compliant shell scripts that must be sourced and need to know their absolute location.Tsana
I'm glad to hear it, @Tsana - thanks for the nice feedback.Suwannee
R
4

@City responded that

DIR=$( cd -P -- "$(dirname -- "$(command -v -- "$0")")" && pwd -P )

works. I used it too.
I found the command at https://mcmap.net/q/13462/-can-i-get-the-absolute-path-to-the-current-script-in-kornshell‌​rrent-script-in-kornshell.

Ratline answered 23/4, 2015 at 21:36 Comment(4)
No harm, but generally other stack posts are referenced in comments, not posted as answers. The SO format for providing the link is [Link Title](url). (you can include normal bold by surrounding the Link Title with double-asterisks) E.g. Can I get the absolute path to the current script in KornShell?Deposit
@David Thanks for format, I did post it as a comment first. I had a response that my comment worked, so I wanted to offer a possibility to close the question. Other people might think City is still looking for an answer now. Can you advise how I should have done it?Ratline
Even if the comment worked, it should be left as a reference in the comment. Why? Because the answer is already on SO, and one huge headache on SO is having negligibly differing versions of the same answer scattered all over SO under different questions. The goal being to have all related answers under a single question so they are easily found/referenced for all. (We will never get there 100%, but when you find the answer under another question, please do not duplicate it yet again)Deposit
@DavidC.Rankin: Note that this question is explicitly about the POSIX shell (sh), whereas the question linked here is about ksh, and the questions linked in comments at the top are bash questions; while some of the answers to those questions happen to contain solutions that are also POSIX-compliant, they aren't discussed as such, and even though this question is closely related, it is distinct, of general interest, and deserves its own, canonical answer here. Perhaps Walter could have copied the ksh-tagged solution and explained why it works with all POSIX-compliant shells.Suwannee
B
1
if [ -f "$0" ]; then script=$0; else script=$(command -v -- "$0"); fi
dir=$(dirname -- "$script")
dir=$(CDPATH=; cd -- "$dir" && pwd -P)

When a relative directory is all I need, as is usual, I omit the last line.

Conditions

The snippet gives the right answer if the script is directly invoked (e.g., script or path/to/script), or if the shell is directly invoked with the script as first argument (sh script or sh path/to/script). You can use any POSIX-compliant shell instead of sh.

The snippet does not give the right answer if the script is sourced (. script or . path/to/script). Some shells, such as bash and ksh93, have nonportable features that will let you get the answer, but it can't be done within POSIX.

The third line resolves directory symlinks; if you don't want to do that, remove the -P after pwd. The snippet does not resolve file symlinks. POSIX provides no simple way to do so.

The snippet must appear before the script cds for the first time.

How it works

The snippet's first line

if [ -f "$0" ]; then script=$0; else script=$(command -v -- "$0"); fi

computes a path to the script. In most cases that satisfy the conditions above, $0 is that path. [ -f "$0" ] yields true, and $0 is assigned to the variable script.

Now suppose your script is invoked via sh script. That is, the shell is directly invoked and the script filename, without a slash character, is the first argument. There are two cases: either the script exists in the current working directory (first case) or it doesn't, but it's on the PATH (second case).

In the first case, $0 is the script filename, which is also a correct path. It's assigned to the variable script, as above.

In the second case, POSIX allows the shell to search the PATH. (Some shells do, like bash and ksh93. Others don't, like dash and mksh. We assume we're in a shell that does.) $0 is again the script filename. But this time it isn't a path to the script, because the script doesn't exist in the cwd! [ -f "$0" ] yields false; command -v finds the script on the PATH; and the result is assigned to the variable script.

The first case is why the first line cannot be reduced to script=$(command -v -- "$0"); that would fail or produce the wrong result if the cwd were not in the PATH. The second case is why the first line cannot be reduced to script=$0.

Let's move on to the second line:

dir=$(dirname -- "$script")

It extracts the directory part of $script. Nothing to talk about there.

Finally, the third line

dir=$(CDPATH=; cd -- "$dir" && pwd -P)

ensures that the directory is absolute, and resolves directory symlinks.

The third line sets CDPATH to empty because otherwise, cd can misbehave. And it does so in a separate statement (CDPATH=; cd) rather than in the same statement (CDPATH= cd) because the latter doesn't have the desired effect in /bin/sh and /bin/ksh on AIX, IBM's Unix.

Beyrouth answered 18/2 at 18:53 Comment(0)
L
0
if      OLDPWD=/dev/fd/0 \
        cd - && ls -lLidFH ?
then    cd . <8
fi      </proc/self/fd 8<. 9<$0

there. that should enable you to change directpry through some magic links as file descriptors.

setting $OLDPWD pre-cd exports the value for the duration of the one change directory (note: cd can have residual effects on hash tables, but the only sh about which i am aware that actually males any good use of these is kevin ahlmquists - and since herbert xu - dash, and maybe some bsd stuff, but what do i know?) but does not carry over any cd exports as a result of the change.

thus, $OLDPWD does not change, actually, and if it had any value at all this remains as was. $PWD is changed as a result of the first cd, and the value becomes /dev/fd/0 which points to /proc/self/fd, where a list of file descriptors for our process should be in ., to include whatever $0 is on ./2.

so we do an ls ... ? and have a look at all of the wonderful info we can get, and we go whence we came.

yay!

Lise answered 24/1, 2019 at 6:0 Comment(4)
The question is about a POSIX-compliant solution, so you cannot rely on Linux filesystem (/proc/self). Unless you run the script sourced - in which you can't rely on $0 anyway - you needn't worry about OLDPWD (which isn't a POSIX-mandated shell variable to begin with). When I run this on Ubuntu 18.04, I get literal /dev/fd/0, followed by a listing of the process' file descriptors (plus an error message about descriptor 3) - I don't see the script's (symlink-resolved) directory path, which is what the question is about.Suwannee
@mklement0: yo. <script> resolves symlinks with either of cd -[LP] for link memories sh local or properly canonical. the former method enables a ln -sf ~/../~\${../tmp/;cd <./0/.. -L [.-\9] or `similar; for CDPATH inLise
@mklement0: yo. <script> resolves symlinks with either of cd -[LP] for link memories sh local or properly canonical. the former method enables a ln -sf <~nobody/onthisfreakyphonedoesntchargeagainanduponwhichicannottestthisstuffforthreeyearswhilemysoniskidnappedbyunitedstatesnavy ~/../~"${HOME##*/}" /dev/fd/0;2<>~/../\~ ... ehh... bored. cant even charge this last vestige of computing available... OLDPWD=/proc/$$/fd/$?/../root<~ cd -L ->>; cd -L ../$?<~somename and so on.. theres another name space. try putting executables in an fd and linkinkg over them... you can mount like that.Lise
This is amazing madness, but it does show the principle, and works, with modification, to output just the symlink target of the file descriptor to itself. I have ksh, dash, bash working easily, zsh not immediately. If we're using Linux anyway, find /proc/$$/fd -lname "*${0##*/}" -printf '%l\n' gives the script itself, then dirname it.Suber

© 2022 - 2024 — McMap. All rights reserved.