Differences between revisions 1 and 26 (spanning 25 versions)
Revision 1 as of 2007-05-02 23:16:02
Size: 5100
Editor: redondos
Comment:
Revision 26 as of 2013-07-03 16:50:27
Size: 8399
Editor: Lhunath
Comment: $0
Deletions are marked like this. Additions are marked like this.
Line 1: Line 1:
[[Anchor(faq28)]] <<Anchor(faq28)>>
Line 3: Line 3:
This is a complex question because there's no single right answer to it. Even worse: it's not possible to find the location reliably in 100% of all cases. All ways of finding a script's location depend on the name of the script, as seen in the predefined variable {{{$0}}}. But providing the script name in {{{$0}}} is only a (very common) convention, not a requirement.
Line 5: Line 4:
The suspect answer is "in some shells, $0 is always an absolute path, even if you invoke the script using a relative path, or no path at all". That's not the case in ["BASH"]. But this isn't reliable across shells; some of them return the actual command typed in by the user instead of the fully qualified path. In those cases, if all you want is the fully qualified version of $0, you can use something like this (["POSIX"], non-Bourne): There are two prime reasons why this issue comes up: you either want to externalize data or configuration of your script and need a way to find these external resources, or your script is intended to act upon a bundle of some sort (eg. a build script), and needs to find the resources to act upon.

It is important to realize that in the general case, this problem has no solution. Any approach you might have heard of, and any approach that will be detailed below has flaws and will only work for their specific cases, so pay attention but first and foremost, try to avoid the problem entirely by not depending on the location of your script!

Before we dive into solutions, let's clear up some misunderstandings. It is important to understand that:

 * Your script does '''not''' actually have a location! Wherever the bytes end up coming from, there is no "one canonical path" for it. Never.
 * {{{$0}}} is NOT the answer to your problem. If you think it is, you can either stop reading and write more bugs, or you can accept this and read on.


=== Accessing data/config files ===

Very often, people want to make their scripts configurable. And the separation principle teaches us that it's a good idea to keep configuration and code separate. The problem then ends up being: how does my script know where to find the user's configuration file for it?

Too often, people believe the configuration of a script should reside in the same directory as where they put their script. This is the root of the problem.

Interestingly, a UNIX paradigm exists to solve this problem for you: Configuration artifacts of your scripts should exist in either the user's {{{HOME}}} directory or {{{/etc}}}. That gives your script an absolute path to look for the file, solving your problem instantly: you no longer depend on the "location" of your script:
Line 8: Line 23:
  [[ $0 = /* ]] && echo $0 || echo $PWD/$0     if [[ -e ~/.myscript.conf ]]; then source ~/.myscript.conf
    elif [[ -e /etc/myscript.conf ]]; then source /etc/myscript.conf
    fi
Line 11: Line 28:
Or the BourneShell version:
=== Acting on a bundle ===

More common yet, scripts are part of a bundle and perform certain actions within or upon it. Ideally, it is desired that the bundle works independently of where the user has unpacked it; whether that's somewhere in their home dir, in {{{/var}}} or in {{{/usr/local}}}.

When our script needs to act upon other files it's bundled with, independently of its absolute location, we have two options: Either we rely on {{{PWD}}} or we rely on {{{BASH_SOURCE}}}. Both approaches have certain issues, here's what you need to know.

==== BASH_SOURCE ====

The {{{BASH_SOURCE}}} internal bash variable is actually an array or pathnames. If you however expand it as a simple string, eg. {{{"$BASH_SOURCE"}}}, you'll get the first element, which is the pathname of the currently executing function or script. The following caveats apply:

 * {{{BASH_SOURCE}}} expands ''empty'' when bash does not know where the executing code comes from. Usually, this means the code is coming from ''standard input'' (eg. ssh host 'somecode', or from an interactive session).
 * {{{BASH_SOURCE}}} does ''not follow'' symlinks (when you run {{{z}}} from {{{/x/y}}}, you get {{{/x/y/z}}}, even if that is a symlink to {{{/p/q/r}}}). Often, this is the desired effect. Sometimes, though, it's not. Imagine your bundle links its start-up script into {{{/usr/local/bin}}}, now that script's {{{BASH_SOURCE}}} will lead you into {{{/usr/local}}} and not into the bundle.

If you're not writing a bash script, the {{{BASH_SOURCE}}} variable is unavailable to you. There is a common convention, however, for passing the location of the script as the process name when it is started. Most shells do this, but not all shells do so reliably or using an absolute path. Relying on this behaviour is dangerous and fragile, but can be done by looking at {{{$0}}}. Again, consider all your options before doing this: you are likely creating more problems than you are solving.

==== PWD ====

Another option is to rely on {{{PWD}}}, the current working directly. In this case, you can assume the user has first {{{cd}}}'ed into your bundle and make all your pathnames relative. To reduce fragility, you could even test whether, for example, the relative path to the script name is correct, to make sure the user has indeed {{{cd}}}'ed into the bundle:
Line 14: Line 49:
  case $0 in /*) echo $0;; *) echo `pwd`/$0;; esac     [[ -e bin/myscript ]] || { echo >&2 "Please cd into the bundle before running this script."; exit 1; }
Line 17: Line 52:
Or a shell-independent variant (needs a {{{readlink}}} supporting {{{-f}}}, though) If you ever do need an absolute path, you can always get one by prefixing the relative path with {{{PWD}}}: {{{echo "Saved to: $PWD/result.csv"}}}

The only difficulty here is that you're now forcing your user to first change into your bundle's directory before your script can function. Regardless, this may well be your best option!

==== Config ====

If neither the {{{BASH_SOURCE}}} or the {{{PWD}}} option sound interesting, you might want to consider going the route of configuration files instead (see the previous section). In this case, you require that your user configure the location of your bundle in a configuration file and have him put that configuration file in a location you can easily find. For example:
Line 20: Line 61:
  readlink -f "$0";     [[ -e ~/.myscript.conf ]] || { echo >&2 "First configure the product in ~/.myscript.conf"; exit 1; }
    source ~/.myscript.conf # ~/.myscript.conf defines something like bundleDir=/x/y
    cd "$bundleDir" # Now you can use the PWD method: use relative paths.
Line 23: Line 66:
However, this approach has some major drawbacks. The most important is, that the script name (as seen in {{{$0}}}) may not be relative to the current working directory, but relative to a directory from the program search path {{{$PATH}}} (this is often seen with KornShell). === Why is it so hard to find my script's location? ===
Line 25: Line 68:
Another drawback is that there is really no guarantee that your script is still in the same place it was when it first started executing. Suppose your script is loaded from a temporary file which is then unlinked immediately... your script might not even exist on disk any more! The script could also have been moved to a different location while it was executing. Or (and this is most likely by far...) there might be multiple links to the script from multiple locations, one of them being a simple symlink from a common {{{PATH}}} directory like {{{/usr/local/bin}}}, which is how it's being invoked. Your script might be in {{{/opt/foobar/bin/script}}} but the naive approach of reading {{{$0}}} won't tell you that. This is a complex question because there's no single right answer to it. Even worse: it's not possible to find the location reliably in 100% of all cases. Common ways of finding a script's location depend on the name of the script, as seen in the predefined variable {{{$0}}} ('''don't do this!'''). But providing the script name in {{{$0}}} is only a (very common) convention, not a requirement.
Line 27: Line 70:
(For a more general discussion of the Unix file system and how symbolic links affect your ability to know where you are at any given moment, see [http://www.cs.bell-labs.com/sys/doc/lexnames.html this Plan 9 paper].) The suspect answer is "in some shells, $0 is always an absolute path, even if you invoke the script using a relative path, or no path at all". But this isn't reliable across shells; some of them (including [[BASH]]) return the actual command typed in by the user instead of the fully qualified path. And this is just the tip of the iceberg!
Line 29: Line 72:
So if the name in {{{$0}}} is a relative one, i.e. does not start with '/', we can still try to search the script like the shell would have done: in all directories from {{{$PATH}}}.

The following script shows how this could be done:
Your script may not actually be on a locally accessible disk ''at all''. Consider this:
Line 34: Line 75:
    myname=$0
    if [ -s "$myname" ] && [ -x "$myname" ]
    then # $myname is already a valid file name
        mypath=$myname
    else
        case "$myname" in
        /*) exit 1;; # absolute path - do not search PATH
        *)
            # Search all directories from the PATH variable. Take
            # care to interpret leading and trailing ":" as meaning
            # the current directory; the same is true for "::" within
            # the PATH.
  ssh remotehost bash < ./myscript
}}}
The shell running on remotehost is getting its commands from a pipe. There's no script ''anywhere'' on any disk that {{{bash}}} can see.
Line 47: Line 79:
            for dir in `echo "$PATH" | sed 's/^:/.:/g;s/::/:.:/g;s/:$/:./;s/:/ /g'`
            do
                [ -f "$dir/$myname" ] || continue # no file
                [ -x "$dir/$myname" ] || continue # not executable
                mypath=$dir/$myname
                break # only return first matching file
            done
            ;;
        esac
    fi
Moreover, even if your script is stored on a local disk and executed, it could ''move''. Someone could {{{mv}}} the script to another location in between the time you type the command and the time your script checks {{{$0}}}. Or someone could have unlinked the script during that same time window, so that it doesn't actually have a link within a file system any more.
Line 58: Line 81:
    if [ -f "$mypath" ]
    then
        : # echo >&2 "DEBUG: mypath=<$mypath>"
    else
        echo >&2 "cannot find full path name: $myname"
        exit 1
    fi
Even in the cases where the script is in a fixed location on a local disk, the {{{$0}}} approach still has some major drawbacks. The most important is that the script name (as seen in {{{$0}}}) may not be relative to the current working directory, but relative to a directory from the program search path {{{$PATH}}} (this is often seen with KornShell). Or (and this is most likely problem by far...) there might be multiple links to the script from multiple locations, one of them being a simple symlink from a common {{{PATH}}} directory like {{{/usr/local/bin}}}, which is how it's being invoked. Your script might be in {{{/opt/foobar/bin/script}}} but the naive approach of reading {{{$0}}} won't tell you that -- it may say {{{/usr/local/bin/script}}} instead.
Line 66: Line 83:
    echo >&2 "path of this script: $mypath" (For a more general discussion of the Unix file system and how symbolic links affect your ability to know where you are at any given moment, see [[http://www.cs.bell-labs.com/sys/doc/lexnames.html|this Plan 9 paper]].)

=== Non-bash solutions ===

{{{
  case $0 in /*) echo "$0";; *) echo "`pwd`/$0";; esac
Line 69: Line 91:
Note that {{{$mypath}}} is not necessarily an absolute path name. It still can contain relative parts like {{{../bin/myscript}}}. Or a shell-independent variant (needs a {{{readlink(1)}}} supporting {{{-f}}}, though, so it's OS-dependent):
Line 71: Line 93:
Generally storing data files in the same directory as their scripts is a bad practice. The Unix file system layout assumes that files in one place (e.g. /bin) are executable programs, while files in another place (e.g. /etc) are data files. (Let's ignore legacy Unix systems with programs in /etc for the moment, shall we....) {{{
  readlink -f "$0"
}}}
Line 73: Line 97:
It really makes the most sense to keep your script's configuration in a single, static location such as {{{$SCRIPTROOT/etc/foobar.conf}}}. If you need to define multiple configuration files, then you can have a directory (say, {{{/var/lib/foobar}}} or {{{/usr/local/lib/foobar}}}), and read that directory's location from a variable in {{{/etc/foobar.conf}}}. If you don't even want that much to be hard-coded, you could pass the location of {{{foobar.conf}}} as a parameter to the script. If you need the script to assume certain default in the absence of {{{/etc/foobar.conf}}}, you can put defaults in the script itself, and/or fall back to something like {{{$HOME/.foobar.conf}}} if {{{/etc/foobar.conf}}} is missing. (This depends on what your script does. In some cases, it may make more sense to abort gracefully.) ----
CategoryShell

How do I determine the location of my script? I want to read some config files from the same place.

There are two prime reasons why this issue comes up: you either want to externalize data or configuration of your script and need a way to find these external resources, or your script is intended to act upon a bundle of some sort (eg. a build script), and needs to find the resources to act upon.

It is important to realize that in the general case, this problem has no solution. Any approach you might have heard of, and any approach that will be detailed below has flaws and will only work for their specific cases, so pay attention but first and foremost, try to avoid the problem entirely by not depending on the location of your script!

Before we dive into solutions, let's clear up some misunderstandings. It is important to understand that:

  • Your script does not actually have a location! Wherever the bytes end up coming from, there is no "one canonical path" for it. Never.

  • $0 is NOT the answer to your problem. If you think it is, you can either stop reading and write more bugs, or you can accept this and read on.

Accessing data/config files

Very often, people want to make their scripts configurable. And the separation principle teaches us that it's a good idea to keep configuration and code separate. The problem then ends up being: how does my script know where to find the user's configuration file for it?

Too often, people believe the configuration of a script should reside in the same directory as where they put their script. This is the root of the problem.

Interestingly, a UNIX paradigm exists to solve this problem for you: Configuration artifacts of your scripts should exist in either the user's HOME directory or /etc. That gives your script an absolute path to look for the file, solving your problem instantly: you no longer depend on the "location" of your script:

    if [[ -e ~/.myscript.conf ]]; then source ~/.myscript.conf
    elif [[ -e /etc/myscript.conf ]]; then source /etc/myscript.conf
    fi

Acting on a bundle

More common yet, scripts are part of a bundle and perform certain actions within or upon it. Ideally, it is desired that the bundle works independently of where the user has unpacked it; whether that's somewhere in their home dir, in /var or in /usr/local.

When our script needs to act upon other files it's bundled with, independently of its absolute location, we have two options: Either we rely on PWD or we rely on BASH_SOURCE. Both approaches have certain issues, here's what you need to know.

BASH_SOURCE

The BASH_SOURCE internal bash variable is actually an array or pathnames. If you however expand it as a simple string, eg. "$BASH_SOURCE", you'll get the first element, which is the pathname of the currently executing function or script. The following caveats apply:

  • BASH_SOURCE expands empty when bash does not know where the executing code comes from. Usually, this means the code is coming from standard input (eg. ssh host 'somecode', or from an interactive session).

  • BASH_SOURCE does not follow symlinks (when you run z from /x/y, you get /x/y/z, even if that is a symlink to /p/q/r). Often, this is the desired effect. Sometimes, though, it's not. Imagine your bundle links its start-up script into /usr/local/bin, now that script's BASH_SOURCE will lead you into /usr/local and not into the bundle.

If you're not writing a bash script, the BASH_SOURCE variable is unavailable to you. There is a common convention, however, for passing the location of the script as the process name when it is started. Most shells do this, but not all shells do so reliably or using an absolute path. Relying on this behaviour is dangerous and fragile, but can be done by looking at $0. Again, consider all your options before doing this: you are likely creating more problems than you are solving.

PWD

Another option is to rely on PWD, the current working directly. In this case, you can assume the user has first cd'ed into your bundle and make all your pathnames relative. To reduce fragility, you could even test whether, for example, the relative path to the script name is correct, to make sure the user has indeed cd'ed into the bundle:

    [[ -e bin/myscript ]] || { echo >&2 "Please cd into the bundle before running this script."; exit 1; }

If you ever do need an absolute path, you can always get one by prefixing the relative path with PWD: echo "Saved to: $PWD/result.csv"

The only difficulty here is that you're now forcing your user to first change into your bundle's directory before your script can function. Regardless, this may well be your best option!

Config

If neither the BASH_SOURCE or the PWD option sound interesting, you might want to consider going the route of configuration files instead (see the previous section). In this case, you require that your user configure the location of your bundle in a configuration file and have him put that configuration file in a location you can easily find. For example:

    [[ -e ~/.myscript.conf ]] || { echo >&2 "First configure the product in ~/.myscript.conf"; exit 1; }
    source ~/.myscript.conf      # ~/.myscript.conf defines something like bundleDir=/x/y
    cd "$bundleDir"              # Now you can use the PWD method: use relative paths.

Why is it so hard to find my script's location?

This is a complex question because there's no single right answer to it. Even worse: it's not possible to find the location reliably in 100% of all cases. Common ways of finding a script's location depend on the name of the script, as seen in the predefined variable $0 (don't do this!). But providing the script name in $0 is only a (very common) convention, not a requirement.

The suspect answer is "in some shells, $0 is always an absolute path, even if you invoke the script using a relative path, or no path at all". But this isn't reliable across shells; some of them (including BASH) return the actual command typed in by the user instead of the fully qualified path. And this is just the tip of the iceberg!

Your script may not actually be on a locally accessible disk at all. Consider this:

  ssh remotehost bash < ./myscript

The shell running on remotehost is getting its commands from a pipe. There's no script anywhere on any disk that bash can see.

Moreover, even if your script is stored on a local disk and executed, it could move. Someone could mv the script to another location in between the time you type the command and the time your script checks $0. Or someone could have unlinked the script during that same time window, so that it doesn't actually have a link within a file system any more.

Even in the cases where the script is in a fixed location on a local disk, the $0 approach still has some major drawbacks. The most important is that the script name (as seen in $0) may not be relative to the current working directory, but relative to a directory from the program search path $PATH (this is often seen with KornShell). Or (and this is most likely problem by far...) there might be multiple links to the script from multiple locations, one of them being a simple symlink from a common PATH directory like /usr/local/bin, which is how it's being invoked. Your script might be in /opt/foobar/bin/script but the naive approach of reading $0 won't tell you that -- it may say /usr/local/bin/script instead.

(For a more general discussion of the Unix file system and how symbolic links affect your ability to know where you are at any given moment, see this Plan 9 paper.)

Non-bash solutions

  case $0 in /*) echo "$0";; *) echo "`pwd`/$0";; esac

Or a shell-independent variant (needs a readlink(1) supporting -f, though, so it's OS-dependent):

  readlink -f "$0"


CategoryShell

BashFAQ/028 (last edited 2022-03-16 22:42:19 by larryv)