2

There are plenty of answers in SO to address this question:

And there are a few similar questions in Unix & Linux for sourced scripts.

Still, some of them are Bash-specific (they rely on $BASH_SOURCE). Some of them don't work if the directory name contains a newline, and some of them don't work well if there are symlinks involved.

What is a reliable way of getting the path to the directory containing the running/invoking script in every major shell? Having a solution that checks for each shell type is fine.

2 Answers2

3

I'm going to make a first-stage stab at this. Someone else will hopefully improve.

Before executing your script, the shell will open a file-descriptor to the file. Usually this is assigned at fd 255. At any rate, if there's an open fd, then lsof can find it. So we use lsof -p $$ and get the highest-file-descriptor's filename. lsof won't work with every flavor of unix. The wiki for BSD says the equivalent there is fstat. It seems to be on Darwin (Mac OS). With `-F

A sample script:

#!/bin/sh

this_script_path=`lsof -p $$  | awk '/\/'${0##*/}'$/' | cut -c 55-`

Obviously, the cutting is very dependent on specific formatting of lsof. We can alleviate this in version 2. BTW: My version of lsof translates unprintable characters so that even tabs in path names get converted to \t.

Version 2. Apologies in advanced for the ugly perl code. This time we're going to use the -F option to control the output. With -F fn we will get output like this:

p3834
fcwd
n/home/joe/test
frtd
n/
ftxt
n/bin/bash
fmem
n/lib64/ld-2.12.so
fmem
n/lib64/libdl-2.12.so
fmem
n/lib64/libc-2.12.so
fmem
n/lib64/libtinfo.so.5.7
fmem
n/usr/lib/locale/locale-archive
fmem
n/usr/lib64/gconv/gconv-modules.cache
f0
n/dev/pts/1
f1
n/dev/pts/1
f2
n/dev/pts/1
f255
n/home/joe/test/t.sh

We've got to convert that mess so that the highest file-descriptor (I'm assuming you can't rely on it being 255) is the script-name. (This seems to work in dash as well.)

this_script_path=`lsof -p $$ -F fn | 
  perl -lane '
        $fd=$1,next if /^f(\d+)/; 
        $p{$fd}=$1 if $fd and /^n(.*)/;
        $fd="";
  }END { 
        @x=sort {$a<=>$b} keys %p;
        print $p{$x[-1]}; 
  }{'`

The perl script is ugly, I agree. It was a one-liner that I broke up for clarity. We capture the number of the file-descriptor if the line begins with f, and we capture the filename into a hash if we have a valid file-descriptor and valid filename. Just in case, if none of those conditions were met, we clear $fd. After all lines are processed, we numerically sort the keys (our file-descriptors) of our hash, store the results into array x and outputs the contents of the hash p of filenames, indexed by the last element (the greatest value) in array x.

The only question is: will lsof be installed on all the systems and how stable is this output format.

Otheus
  • 6,138
  • That's a nice heuristic, which works for dash, yash, posh, ksh93, mksh, bash and zsh. But it does fail in some edge cases, such as if the script is called with a file descriptor >9 already open. It also doesn't detect that the shell was executed with sh <script-file or sh -c 'script content'. – Gilles 'SO- stop being evil' May 12 '15 at 23:31
  • @Gilles Cool. thanks for the caveats. For the edge cases you mentioned, lsof -p $$ might still work, but the fd won't be the last. – Otheus May 12 '15 at 23:35
  • lsof -p$$ always “works” if there is a script file, but that doesn't do you any good without a method to pick the right descriptor. “Pick the highest one” works often, but not always and there's no way to detect the edge cases. – Gilles 'SO- stop being evil' May 12 '15 at 23:48
  • The ZEN method: pick the file descriptor that most looks like it knows what it's doing. – Otheus May 12 '15 at 23:52
1

There are heuristics that can help you, but there is no fully reliable way.

Otheus shows how to use file descriptors. That's a nice heuristic, which works in most cases. However there are edge cases where it fails, and there's no way to detect failures.

Example: take the following script.

#!/bin/sh
set
lsof -p$$ | sed 's/[0-9][0-9]*//'

Make two copies of the script, one called foo, one called bar. Now let's stress this a bit:

$ env -i PATH=/bin:/usr/bin perl -MPOSIX -e 'dup2(4, 11) or die $!; exec "dash", "foo", "bar"' 3<foo 4<bar </dev/null >foo.out
$ env -i PATH=/bin:/usr/bin perl -MPOSIX -e 'dup2(3, 10) or die $!; exec "dash", "bar", "foo"' 3<foo 4<bar </dev/null >bar.out
$ diff foo.out bar.out

17c17 < dash gilles 1w REG 0,24 99 10130024 /tmp/202954/foo.out ---

dash gilles 1w REG 0,24 99 10130022 /tmp/202954/bar.out

The only difference here is the file in which I logged the output.

Another case where this heuristic would fail is if the shell was invoked on standard input, or with -c.

Another approach is to parse the shell's command line. On Linux, you can access it through /proc. The arguments are null-delimited, which is hard to parse with portable shell tools (recent GNU tools make it easier), but it can be done. Portably, you need to call ps to access the arguments, and there is no output format that is unambiguous: ps -o args concatenates the arguments with spaces, and may be truncated.

Even on Linux, the problem you'll run into here is that the shell might have been invoked with options that your script isn't aware of, and one of these options might take an argument. For example, suppose you have a script called 1 and another script called 2.

mksh -T 1 2

This invokes mksh on /dev/tty1 and runs the script 2.

zsh -T 1 2

This invokes zsh with the cprecedences option and runs the script 1 with the argument 2.

You need knowledge of individual shells to tell these apart. With this method, you can detect edge cases: if you see a nonstandard option, bail out.