Differences between revisions 1 and 19 (spanning 18 versions)
Revision 1 as of 2007-05-02 23:16:02
Size: 5100
Editor: redondos
Comment:
Revision 19 as of 2011-08-20 22:43:04
Size: 7476
Editor: dslb-084-056-243-161
Comment:
Deletions are marked like this. Additions are marked like this.
Line 1: Line 1:
[[Anchor(faq28)]] <<Anchor(faq28)>>
Line 3: Line 4:
This topic comes up frequently. This answer covers not only the expression used above ("configuration files"), but also several variant situations. If you've been directed here, please read this entire answer before dismissing it.
Line 5: Line 8:
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): 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:
Line 8: Line 13:
  [[ $0 = /* ]] && echo $0 || echo $PWD/$0   ssh remotehost bash < ./myscript
Line 10: Line 15:
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 11: Line 17:
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 [[http://www.cs.bell-labs.com/sys/doc/lexnames.html|this Plan 9 paper]].)

Having said all that, if you ''still'' want to make a whole slew of naive assumptions, and all you want is the fully qualified version of $0, you can use something like this ([[BASH]] syntax):

{{{
  [[ $0 == /* ]] && echo "$0" || echo "${PWD}/${0#./}"
}}}
Line 14: Line 31:
  case $0 in /*) echo $0;; *) echo `pwd`/$0;; esac   case $0 in /*) echo "$0";; *) echo "$(pwd)/$0";; esac
Line 16: Line 33:

Or a shell-independent variant (needs a {{{readlink}}} supporting {{{-f}}}, though)
Or a shell-independent variant (needs a {{{readlink(1)}}} supporting {{{-f}}}, though, so it's OS-dependent):
Line 20: Line 36:
  readlink -f "$0";   readlink -f "$0"
Line 22: Line 38:
In Bash, version 4.1.7(1)-release, on Linux, it seems bash always opens the script with fd 255 so you can just do:
Line 23: Line 40:
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).

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.

(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].)

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}}}.
{{{
  HOME="$(dirname "$(readlink /proc/$$/fd/255)")"
}}}
If we want to account for the cases where the script's relative pathname (in {{{$0}}}) may be relative to a {{{$PATH}}} component instead of the current working directory (as mentioned above), we can try to search for the script in all the directories of {{{$PATH}}}.
Line 33: Line 47:
{{{
    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.
{{{#!highlight sh
#!/bin/bash
Line 47: Line 50:
            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
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.

        # Replace leading : with . in PATH, store in p
        p=${PATH/#:/.:}
        # Replace trailing : with .
        p=${p//%:/:.}
        # Replace :: with :.:
        p=${p//::/:.:}
        # Temporary input field separator, see FAQ #1
        OFS=$IFS IFS=:
        # Split the path on colons and loop through each of them
        for dir in $p; do
                [[ -f "$dir/$myname" ]] || continue # no file
                [[ -x "$dir/$myname" ]] || continue # not executable
Line 53: Line 78:
     done
            ;;
        esac
    fi
        done
        # Restore old input field separator
        IFS=$OFS
    
;;
    esac
fi
Line 58: Line 85:
    if [ -f "$mypath" ]
   
then
        : # echo >&2 "DEBUG: mypath=<$mypath>"
    else
    
echo >&2 "cannot find full path name: $myname"
        exit 1
    fi
if [[ ! -f "$mypath" ]]; then
    echo >&2 "cannot find full path name: $myname"
    exit 1
fi
Line 66: Line 90:
    echo >&2 "path of this script: $mypath" echo >&2 "path of this script: $mypath"
Line 68: Line 92:
Note that {{{$mypath}}} is not necessarily an absolute path name. It still can contain relative paths like {{{../bin/myscript}}}, because {{{$PATH}}} could contain those. If you want to get the directory only from that string, check [[BashFAQ/073|FAQ 73]].
Line 69: Line 94:
Note that {{{$mypath}}} is not necessarily an absolute path name. It still can contain relative parts like {{{../bin/myscript}}}. Are you starting to see how ridiculously complex this problem is becoming? And this is ''still'' just the simplistic case where we've made a lot of assumptions about the script not moving and not being piped in!
Line 71: Line 96:
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....) Generally, storing data files in the same directory as their programs is a bad practise. 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....)
Line 73: Line 98:
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.) Here are some common sense alternatives you should consider, instead of attempting to perform the impossible:

 * It really makes the most sense to keep your script's configuration in a single, static location such as {{{/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 fixed place such as {{{/etc/foobar.conf}}}.
 * If you don't even want that much to be hard-coded, you could pass the location of {{{foobar.conf}}} (or of your configuration directory itself) 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, or fall back to something like {{{$HOME/.foobar.conf}}} if {{{/etc/foobar.conf}}} is missing.
 * When you install the script on a target system, you could put the script's location into a variable in the script itself. The information is available at that point, and as long as the script doesn't move, it will always remain correct for each installed system.
 * In most cases, it makes more sense to abort gracefully if your configuration data can't be found by obvious means, rather than going through arcane processes and possibly coming up with wrong answers.
  . BASH_SOURCE is probably a much better idea than $0, gives better results and is better defined. This article should probably be rewritten with BASH_SOURCE in mind. --[[Lhunath]]

----
CategoryShell

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

This topic comes up frequently. This answer covers not only the expression used above ("configuration files"), but also several variant situations. If you've been directed here, please read this entire answer before dismissing it.

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.

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

Having said all that, if you still want to make a whole slew of naive assumptions, and all you want is the fully qualified version of $0, you can use something like this (BASH syntax):

  [[ $0 == /* ]] && echo "$0" || echo "${PWD}/${0#./}"

Or the BourneShell version:

  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"

In Bash, version 4.1.7(1)-release, on Linux, it seems bash always opens the script with fd 255 so you can just do:

  HOME="$(dirname "$(readlink /proc/$$/fd/255)")"

If we want to account for the cases where the script's relative pathname (in $0) may be relative to a $PATH component instead of the current working directory (as mentioned above), we can try to search for the script in all the directories of $PATH.

The following script shows how this could be done:

   1 #!/bin/bash
   2 
   3 myname=$0
   4 if [[ -s "$myname" ]] && [[ -x "$myname" ]]; then
   5     # $myname is already a valid file name
   6 
   7     mypath=$myname
   8 else
   9     case "$myname" in
  10     /*) exit 1;;             # absolute path - do not search PATH
  11     *)
  12         # Search all directories from the PATH variable. Take
  13         # care to interpret leading and trailing ":" as meaning
  14         # the current directory; the same is true for "::" within
  15         # the PATH.
  16 
  17         # Replace leading : with . in PATH, store in p
  18         p=${PATH/#:/.:}
  19         # Replace trailing : with .
  20         p=${p//%:/:.}
  21         # Replace :: with :.:
  22         p=${p//::/:.:}
  23         # Temporary input field separator, see FAQ #1
  24         OFS=$IFS IFS=:
  25         # Split the path on colons and loop through each of them
  26         for dir in $p; do
  27                 [[ -f "$dir/$myname" ]] || continue # no file
  28                 [[ -x "$dir/$myname" ]] || continue # not executable
  29                 mypath=$dir/$myname
  30                 break           # only return first matching file
  31         done
  32         # Restore old input field separator
  33         IFS=$OFS
  34         ;;
  35     esac
  36 fi
  37 
  38 if [[ ! -f "$mypath" ]]; then
  39     echo >&2 "cannot find full path name: $myname"
  40     exit 1
  41 fi
  42 
  43 echo >&2 "path of this script: $mypath"

Note that $mypath is not necessarily an absolute path name. It still can contain relative paths like ../bin/myscript, because $PATH could contain those. If you want to get the directory only from that string, check FAQ 73.

Are you starting to see how ridiculously complex this problem is becoming? And this is still just the simplistic case where we've made a lot of assumptions about the script not moving and not being piped in!

Generally, storing data files in the same directory as their programs is a bad practise. 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....)

Here are some common sense alternatives you should consider, instead of attempting to perform the impossible:

  • It really makes the most sense to keep your script's configuration in a single, static location such as /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 fixed place such as /etc/foobar.conf.

  • If you don't even want that much to be hard-coded, you could pass the location of foobar.conf (or of your configuration directory itself) 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, or fall back to something like $HOME/.foobar.conf if /etc/foobar.conf is missing.

  • When you install the script on a target system, you could put the script's location into a variable in the script itself. The information is available at that point, and as long as the script doesn't move, it will always remain correct for each installed system.
  • In most cases, it makes more sense to abort gracefully if your configuration data can't be found by obvious means, rather than going through arcane processes and possibly coming up with wrong answers.
    • BASH_SOURCE is probably a much better idea than $0, gives better results and is better defined. This article should probably be rewritten with BASH_SOURCE in mind. --Lhunath


CategoryShell

BashFAQ/028 (last edited 2019-06-03 14:26:04 by 151)