简体   繁体   中英

One-liner to determine the directory of the currently running bash script

This question is related to: Getting the source directory of a Bash script from within - in that question, there is a perfect answer that shows show to get the directory of the currently running bash script (which includes reading the real location of symlinks).

SOURCE="${BASH_SOURCE[0]}"
while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink
  DIR="$( cd -P "$( dirname "$SOURCE" )" >/dev/null && pwd )"
  SOURCE="$(readlink "$SOURCE")"
  [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located
done
DIR="$( cd -P "$( dirname "$SOURCE" )" >/dev/null && pwd )"

This works perfectly, except that it is a lot of boilerplate code. I'm working on a script package that contains lots of small scripts that are using each other. This script package is hosted in a git repo, and it must be location independent . Eg it should always work the same way, regardless of the directory it was cloned out to. One must be able to clone this repo into many different directories and use them independently. Many scripts are just wrappers around commands, or they are calling other scripts in the same directory, or relative to the containing directory . For example, here is a command that simulates the 'mongo' command, but runs it in a docker container:

#!/bin/bash

set -e

SOURCE="${BASH_SOURCE[0]}"
while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink
  DIR="$( cd -P "$( dirname "$SOURCE" )" >/dev/null && pwd )"
  SOURCE="$(readlink "$SOURCE")"
  [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located
done
DIR="$( cd -P "$( dirname "$SOURCE" )" >/dev/null && pwd )"
# mongo-container is another script that determines the container id
# and it must be referenced relatively to the containing directory
MONGOE="docker exec -ti `${DIR}/mongo-container`"
${MONGOE} mongo "$@"

90% of this code is used to determine the directory of the script, only 10% does something useful. I have about 20 scripts like this. It means that 90% of my bash scripts do nothing just "determine the contaning directory". When I look at them, I almost can't see what they do, because there is so much boilerplate code. There must be a better way, I just can't figure out.

Any ideas?

Maybe you would like to 'factorize' this source code, in a dedicated common GNU/Bash script?

First, I think you may use the which command which is available on most of GNU operating system (often without additional installation)

https://savannah.gnu.org/projects/which/

Then you can create a 'common functionalities' file containing the source code you use to define the complete path, including symbolic link management, let's call it /tmp/myTrueDir/common.sh :

#!/bin/bash

set -e

# Usage: resolveCompleteAbsolutePath <$0 path to regard>
function resolveCompleteAbsolutePath() {
  local SOURCE="$1"
  while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink
    DIR="$( cd -P "$( dirname "$SOURCE" )" >/dev/null && pwd )"
    SOURCE="$(readlink "$SOURCE")"
    [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located
  done
  DIR="$( cd -P "$( dirname "$SOURCE" )" >/dev/null && pwd )"

  echo "$DIR"
}

Then you may use this at the beginning of all your little scripts (first we don't care having to manage symbolic links to reach the common.sh file which is in the same directory of your scripts)

currentDir=$( dirname "$( which "$0" )" )
source "$currentDir/common.sh"

Eventually, you can use the resolveCompleteAbsolutePath function (defined in your common.sh file), to get the complete path, including symbolic links resolution.

Such a way, your script will only contain the interesting source code you want, and the path management will be factorized in the same place.

For instance, you can easily test all this with this file system structure:

/tmp/myTrueDir/
/tmp/myTrueDir/common.sh
/tmp/myTrueDir/test.sh

With /tmp/myTrueDir/test.sh file having these sample lines:

#!/bin/bash

currentDir=$( dirname "$( which "$0" )" )
source "$currentDir/common.sh"
resolvedPath=$( resolveCompleteAbsolutePath "$0" )

echo "currentDir: $currentDir"
echo "resolvedPath: $resolvedPath"

Then you can create a symbolic link to your true directory, let's say:

ln -s /tmp/myTrueDir /tmp/mySymbDir

Then you can call your test script from the symbolic path (ie /tmp/mySymbDir/test.sh), and see it works like you want:

currentDir: /tmp/mySymbDir
resolvedDir: /tmp/myTrueDir

See BashFAQ/028 (How do I determine the location of my script? ...) for a good treatment of this problem. In particular, note the second paragraph: "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 in specific cases. First and foremost, try to avoid the problem entirely by not depending on the location of your script!".

It may be that your situation is such that a good enough solution is possible. If your systems have a readlink that supports the -e option, try:

prog_realpath=$(readlink -e -- "${BASH_SOURCE[0]}")
prog_realdir=${prog_realpath%/*}/

Note that the value in prog_realpath will be wrong if the "real" path to the program ends with a newline character. That can be fixed, but it's a very unlikely situation, and it doesn't stop the value in prog_realdir being correct.

See Correct Bash and shell script variable capitalization for the reasons why I changed the names of SOURCE and DIR .

The trailing / on the prog_realdir value is to ensure that it is valid even if the program is in the root directory. Using dirname to get the directory would avoid the / problem, but is vulnerable to the trailing newline problem unless additional trickery is used. See shell: keep trailing newlines ('\\n') in command substitution for more information about the trailing newline problem.

If your systems don't (all) support readlink -e , you may be able to use realpath instead. See What's the difference between “realpath” and “readlink -f” for more information about the availability of readlink and realpath on several OSes.

I tend to put the following line in my code

PROGDIR=$(cd $(dirname $0) && pwd)

This changes to the directory of the script and then runs pwd to get the directory path. I've never had issues with this.

Hope this helps.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM