9

Is it possible that a sourced shell script knows its location? I've read determining path to sourced shell script but the answers focus on bash and tcsh and fail if a POSIX shell is used. $0 is also not the solution and yields wrong results.

A solution doesn't need to be 100% reliable. It's unlikely that the path contains hard or symlinks.

# sourcing the script should yield the absolute path to the script
. somedir/thescript

# within “thescript”
-> /tmp/foo/bar/somedir

Some background: The script is part of an existing application containing dozens of binaries in a bin directory in a known location (varies per architecture) relative to the sourced script. To use the application the user sources the script which prepends the bin directory to the PATH, so the application's binaries can be easily invoked in the current shell (whichever shell that might be).

Marco
  • 33,548
  • Please once check this link might be what you are looking for http://paste.ubuntu.com/6242655/ – Rahul Patil Oct 15 '13 at 21:45
  • @RahulPatil That only works on bash and is highly unportable. – Marco Oct 15 '13 at 21:54
  • 1
    I'm pretty sure that the answer is no. You only found shell-specific methods because there is no portable method. What do you need this for? Can you tell users of the script to do something different from . /path/to/script such as MARCO_DIR=/path/to; . $MARCO_DIR/script or eval "$(/path/to/script)"? @RahulPatil BASH_SOURCE is obviously specific to bash. – Gilles 'SO- stop being evil' Oct 15 '13 at 21:54
  • @Gilles As I said, it doesn't need to be very robust. But I haven't found a way which even remotely works in the simplest cases in a POSIX shell. (So the solution for the time being is that the use of this application requires one of the supported shells.) Feel free to post “no…” as an answer if that is the answer. – Marco Oct 15 '13 at 22:04
  • @Gilles Changing the way the application is set up is not an option. It would break the installations. That's why I would have liked to make the script more robust without changing the way it's called. In the end it just sets the PATH, so if the script fails, the fallback is to find the binaries and add their path to the environment. – Marco Oct 15 '13 at 22:11
  • Could you possibly make a switch (case) which would handle all known and expected shells? – peterph Oct 15 '13 at 22:29
  • @peterph That's how it's done at the moment, but when invoked with a POSIX shell it reaches the end of the switch and fails since it can't find its path. – Marco Oct 15 '13 at 22:32

2 Answers2

10

The location of the sourced script is not available unless you are using a shell that offers extensions to the POSIX specification. You can test this with the following snippet:

env -i PATH=/usr/bin:/bin sh -c '. ./included.sh' | grep included

where included.sh contains

echo "$0"
set

In bash, the name of the sourced script is in $BASH_SOURCE. In zsh (in zsh compatibility mode, not in sh or ksh compatibility mode), it's in $0 (note that in a function, $0 is the function name instead). In pdksh and dash, it isn't available. In ksh93, this method doesn't reveal the solution, but the full path to the included script is available as ${.sh.file}.

If requiring bash or ksh93 or zsh is good enough, you can use this snippet:

if [ -n "$BASH_SOURCE" ]; then
  this_script=$BASH_SOURCE
elif [ -n "$ZSH_VERSION" ]; then
  setopt function_argzero
  this_script=$0
elif eval '[[ -n ${.sh.file} ]]' 2>/dev/null; then
  eval 'this_script=${.sh.file}'
else
  echo 1>&2 "Unsupported shell. Please use bash, ksh93 or zsh."
  exit 2
fi

You can try to guess the location of the script by looking at what files the shell has open. Experimentally this seems to work with dash and pdksh but not with bash or ksh93 which at least for a short script have closed the script file by the time they get around to executing it.

  open_file=$(lsof -F n -p $$ | sed -n '$s/^n//p')
  if [ -n "$open_file" ]; then
    # best guess: $open_file is this script
  fi

The script may not be the file with the highest-numbered descriptor if the script is sourced inside a complex script that has been playing with redirections. You may want to loop through the open files. This is not guaranteed to work anyway. The only reliable way to locate a sourced script is to use bash, ksh93 or zsh.


If you can change the interface, then instead of sourcing your script, have your script print out a shell snippet to be passed to eval in the caller. This is what scripts to set environment variables typically do. It allows your script to be written independently of the vagaries of the caller's shell and shell configuration.

#!/bin/sh
FOO_DIR=$(dirname -- "$0")
cat <<EOF
FOO_DIR='$(printf %s "$FOO_DIR" | sed "s/'/'\\''/g")'
PATH="\$PATH:$FOO_DIR/bin";
export FOO_DIR PATH
EOF

In the caller: eval "`/path/to/setenv`"

  • In ksh/sh you can do this_script=$(realpath /proc/$$/fd/10) – Marcin Raczyński Sep 29 '20 at 15:03
  • @MarcinRaczyński This is not guaranteed. Even if you're willing to assume that the shell hasn't closed the script (this isn't guaranteed, and could break after an upgrade), the file descriptor number of the script depends on the environment. If fd 10 is already taken when the script runs, the script will have to be on a different file descriptor. – Gilles 'SO- stop being evil' Sep 29 '20 at 15:37
-1

Adding to Gilles answer, you MAY be able to get the shell script (if $0 = ash) through the /proc/$$ interface. I don't know how accurate this is, but /proc/$$/fd/11 seems to always point to this_script with BusyBox's ash.

Providing this as a potential answer to those who come across this page (as did I).

Last answer was deleted -- so, my claim to this working is tested on 4 different HW platforms (x86, ARM, XLP, and powerpc). All give the same answer of fd/11. A readlink from /proc/$$/fd/11 yields my script.

Andy

jesse_b
  • 37,005
  • 1
    Most (but not all) shells tend to open the script on file descriptor 10, not 11. They keep file descriptors 0–9 clear because those are for redirections inside the script, and 10 is the first candidate. If you're seeing fd 11, it's probably because there's already something else on fd 10. – Gilles 'SO- stop being evil' Sep 29 '20 at 15:39