Jiaaro
Jiaaro

Reputation: 76938

How do I get the directory where a Bash script is located from within the script itself?

How do I get the path of the directory in which a Bash script is located, inside that script?

I want to use a Bash script as a launcher for another application. I want to change the working directory to the one where the Bash script is located, so I can operate on the files in that directory, like so:

$ ./application

Upvotes: 6347

Views: 2731146

Answers (30)

scrat.squirrel
scrat.squirrel

Reputation: 3836

Incredible how simple can be, no matter how you call the script:

#!/bin/bash
#

the_source=$(readlink -f -- "${BASH_SOURCE[0]}")
the_dirname=$(dirname "${the_source}")

echo "the_source: ${the_source}"
echo "the_dirname: ${the_dirname}"

Run from anywhere:

user@computer:~/Downloads/temp$ ./test.sh 

Output:

the_source: /home/user/Downloads/temp/test.sh
the_dirname: /home/user/Downloads/temp

Upvotes: 15

Doncho Gunchev
Doncho Gunchev

Reputation: 2239

Let me offer a solution that works even if the bash script ends with new line and the directory it resides in also does.

src=$(readlink -f -- "${BASH_SOURCE[0]}"; echo -n .); src="${src%??}"; src=$(dirname "$src"; echo -n .); src="${src%??}"; cd "$src"

or more verbose

src=$(readlink -f -- "${BASH_SOURCE[0]}" && echo -n .)
src="${src%??}"
src=$(dirname "$src" && echo -n .)
src="${src%??}"
cd "$src"

The echo -n . ensures the new lines do not get stripped by the command substitution "$(…)". The src="${src%??}" removes that extra dot and the last new line, which will be added by both readlink and dirname, thus leaving only the real path, even with new lines at the end.

Many thanks to everyone else who pointed out the use of "${BASH_SOURCE[0]}" instead of "$0". Are there any corner cases I am missing here (except readlink and dirname not being available)?

Upvotes: 0

izissise
izissise

Reputation: 953

This is the smallest form I could come up with

CWD=$(cd "${0%/*}"; pwd)

Upvotes: 1

denbon05
denbon05

Reputation: 41

Only the name of the directory where script is located:

DIR_NAME=$(dirname -- "${BASH_SOURCE[0]}")

Absolute dir path path:

ABSOLUTE_DIR_PATH=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )

Upvotes: 0

Gabriel Staples
Gabriel Staples

Reputation: 52985

For Python, see my other answer here.

For Bash, see below:

Summary:

Here is a full program:

my_script.sh:

#!/usr/bin/env bash

# (Commented out because using index 0, as shown farther below, is generally 
# better)
# FULL_PATH_TO_SCRIPT="$(realpath "${BASH_SOURCE[-1]}")"

# OR, if you do NOT need it to work for **sourced** scripts too:
# FULL_PATH_TO_SCRIPT="$(realpath "$0")"

# [Generally prefer this, using index `0`, over the one above using 
# index `-1`]
#
# OR, depending on which path you want, in case of nested `source` calls, 
# use index `0` instead of `-1`: 
# - NB: this may fix the error `No such file or directory` when sourcing
#   this script from another script, where this script sources another
#   file based on the path to its own `FULL_PATH_TO_SCRIPT`, but so does
#   the other script sourcing this script, based on its own outer 
#   `FULL_PATH_TO_SCRIPT`. 
FULL_PATH_TO_SCRIPT="$(realpath "${BASH_SOURCE[0]}")"

# OR, add `-s` to NOT expand symlinks in the path:
# FULL_PATH_TO_SCRIPT="$(realpath -s "${BASH_SOURCE[-1]}")"

SCRIPT_DIRECTORY="$(dirname "$FULL_PATH_TO_SCRIPT")"
SCRIPT_FILENAME="$(basename "$FULL_PATH_TO_SCRIPT")"


# Going further: also obtain the filename with and without the extension
SCRIPT_FILENAME_STEM="${SCRIPT_FILENAME%.*}"        # withOUT the extension
SCRIPT_FILENAME_EXTENSION="${SCRIPT_FILENAME##*.}"  # JUST the extension

# debug prints of all of the above
echo "FULL_PATH_TO_SCRIPT:       $FULL_PATH_TO_SCRIPT"
echo "SCRIPT_DIRECTORY:          $SCRIPT_DIRECTORY"
echo "SCRIPT_FILENAME:           $SCRIPT_FILENAME"
echo "SCRIPT_FILENAME_STEM:      $SCRIPT_FILENAME_STEM"
echo "SCRIPT_FILENAME_EXTENSION: $SCRIPT_FILENAME_EXTENSION"

Mark your program above as executable:

chmod +x my_script.sh

Run it:

./my_script.sh

Example run command and output:

~/GS/dev/eRCaGuy_PathShortener$ ./my_script.sh 
FULL_PATH_TO_SCRIPT:       /home/gabriel/GS/dev/eRCaGuy_PathShortener/my_script.sh
SCRIPT_DIRECTORY:          /home/gabriel/GS/dev/eRCaGuy_PathShortener
SCRIPT_FILENAME:           my_script.sh
SCRIPT_FILENAME_STEM:      my_script
SCRIPT_FILENAME_EXTENSION: sh

Details:

How to obtain the full file path, full directory, and base filename of any script being run OR sourced...

...even when the called script is called from within another bash function or script, or when nested sourcing is being used!

For many cases, all you need to acquire is the full path to the script you just called. This can be easily accomplished using realpath. Note that realpath is part of GNU coreutils. If you don't have it already installed (it comes default on Ubuntu), you can install it with sudo apt update && sudo apt install coreutils.

get_script_path.sh (for the latest version of this script, see get_script_path.sh in my eRCaGuy_hello_world repo):

#!/bin/bash

# A. Obtain the full path, and expand (walk down) symbolic links
# A.1. `"$0"` works only if the file is **run**, but NOT if it is **sourced**.
# FULL_PATH_TO_SCRIPT="$(realpath "$0")"
# A.2. `"${BASH_SOURCE[-1]}"` works whether the file is sourced OR run, and even
# if the script is called from within another bash function!
# NB: if `"${BASH_SOURCE[-1]}"` doesn't give you quite what you want, use
# `"${BASH_SOURCE[0]}"` instead in order to get the first element from the array.
FULL_PATH_TO_SCRIPT="$(realpath "${BASH_SOURCE[-1]}")"
# B.1. `"$0"` works only if the file is **run**, but NOT if it is **sourced**.
# FULL_PATH_TO_SCRIPT_KEEP_SYMLINKS="$(realpath -s "$0")"
# B.2. `"${BASH_SOURCE[-1]}"` works whether the file is sourced OR run, and even
# if the script is called from within another bash function!
# NB: if `"${BASH_SOURCE[-1]}"` doesn't give you quite what you want, use
# `"${BASH_SOURCE[0]}"` instead in order to get the first element from the array.
FULL_PATH_TO_SCRIPT_KEEP_SYMLINKS="$(realpath -s "${BASH_SOURCE[-1]}")"

# You can then also get the full path to the directory, and the base
# filename, like this:
SCRIPT_DIRECTORY="$(dirname "$FULL_PATH_TO_SCRIPT")"
SCRIPT_FILENAME="$(basename "$FULL_PATH_TO_SCRIPT")"

# Now print it all out
echo "FULL_PATH_TO_SCRIPT = \"$FULL_PATH_TO_SCRIPT\""
echo "SCRIPT_DIRECTORY    = \"$SCRIPT_DIRECTORY\""
echo "SCRIPT_FILENAME     = \"$SCRIPT_FILENAME\""

IMPORTANT note on nested source calls: if "${BASH_SOURCE[-1]}" above doesn't give you quite what you want, try using "${BASH_SOURCE[0]}" instead. The first (0) index gives you the first entry in the array, and the last (-1) index gives you the last last entry in the array. Depending on what it is you're after, you may actually want the first entry. I discovered this to be the case when I sourced ~/.bashrc with . ~/.bashrc, which sourced ~/.bash_aliases with . ~/.bash_aliases, and I wanted the realpath (with expanded symlinks) to the ~/.bash_aliases file, NOT to the ~/.bashrc file. Since these are nested source calls, using "${BASH_SOURCE[0]}" gave me what I wanted: the expanded path to ~/.bash_aliases! Using "${BASH_SOURCE[-1]}", however, gave me what I did not want: the expanded path to ~/.bashrc.

Example command and output:

  1. Running the script:
    ~/GS/dev/eRCaGuy_hello_world/bash$ ./get_script_path.sh 
    FULL_PATH_TO_SCRIPT = "/home/gabriel/GS/dev/eRCaGuy_hello_world/bash/get_script_path.sh"
    SCRIPT_DIRECTORY    = "/home/gabriel/GS/dev/eRCaGuy_hello_world/bash"
    SCRIPT_FILENAME     = "get_script_path.sh"
    
  2. Sourcing the script with . get_script_path.sh or source get_script_path.sh (the result is the exact same as above because I used "${BASH_SOURCE[-1]}" in the script instead of "$0"):
    ~/GS/dev/eRCaGuy_hello_world/bash$ . get_script_path.sh 
    FULL_PATH_TO_SCRIPT = "/home/gabriel/GS/dev/eRCaGuy_hello_world/bash/get_script_path.sh"
    SCRIPT_DIRECTORY    = "/home/gabriel/GS/dev/eRCaGuy_hello_world/bash"
    SCRIPT_FILENAME     = "get_script_path.sh"
    

If you use "$0" in the script instead of "${BASH_SOURCE[-1]}", you'll get the same output as above when running the script, but this undesired output instead when sourcing the script:

~/GS/dev/eRCaGuy_hello_world/bash$ . get_script_path.sh 
FULL_PATH_TO_SCRIPT               = "/bin/bash"
SCRIPT_DIRECTORY                  = "/bin"
SCRIPT_FILENAME                   = "bash"

And, apparently if you use "$BASH_SOURCE" instead of "${BASH_SOURCE[-1]}", it will not work if the script is called from within another bash function. So, using "${BASH_SOURCE[-1]}" is therefore the best way to do it, as it solves both of these problems! See the references below.

Difference between realpath and realpath -s:

Note that realpath also successfully walks down symbolic links to determine and point to their targets rather than pointing to the symbolic link. If you do NOT want this behavior (sometimes I don't), then add -s to the realpath command above, making that line look like this instead:

# Obtain the full path, but do NOT expand (walk down) symbolic links; in
# other words: **keep** the symlinks as part of the path!
FULL_PATH_TO_SCRIPT="$(realpath -s "${BASH_SOURCE[-1]}")"

This way, symbolic links are NOT expanded. Rather, they are left as-is, as symbolic links in the full path.

The code above is now part of my eRCaGuy_hello_world repo in this file here: bash/get_script_path.sh. Reference and run this file for full examples both with and withOUT symlinks in the paths. See the bottom of the file for example output in both cases.

References:

  1. How to retrieve absolute path given relative
  2. taught me about the BASH_SOURCE variable: Unix & Linux: determining path to sourced shell script
  3. taught me that BASH_SOURCE is actually an array, and we want the last element from it for it to work as expected inside a function (hence why I used "${BASH_SOURCE[-1]}" in my code here): Unix & Linux: determining path to sourced shell script
  4. man bash --> search for BASH_SOURCE:

    BASH_SOURCE

    An array variable whose members are the source filenames where the corresponding shell function names in the FUNCNAME array variable are defined. The shell function ${FUNCNAME[$i]} is defined in the file ${BASH_SOURCE[$i]} and called from ${BASH_SOURCE[$i+1]}.

See also:

  1. My answer for Python: How do I get the path and name of the python file that is currently executing?
  2. [my answer] Unix & Linux: determining path to sourced shell script

Upvotes: 31

James
James

Reputation: 80

What problems will this script cause ?

#!/bin/bash
x=`which "${0}"`
echo basename = `basename "${x}"`
echo dirname = `dirname "${x}"`
echo current pwd = `pwd`

I put this script into ~/path with space/

$ ls -alFd ~/p*

drwxr-xr-x 1 None 0 '/home/Asus/path with space'/

test :

$ pwd 
/home/Asus 

$ bash ~/path\ with\ space/test.sh 
basename = test.sh 
dirname = /home/Asus/path with space 
current pwd = /home/Asus 
  
$ ~/path\ with\ space/test.sh 
basename = test.sh 
dirname = /home/Asus/path with space 
current pwd = /home/Asus 
 
$ ./path\ with\ space/test.sh 
basename = test.sh 
dirname = /home/Asus/path with space 
current pwd = /home/Asus 
  
$ bash ./path\ with\ space/test.sh 
basename = test.sh 
dirname = /home/Asus/path with space 
current pwd = /home/Asus 
  
  
$ cd /tmp 
 
$ bash ~/path\ with\ space/test.sh 
basename = test.sh 
dirname = /home/Asus/path with space 
current pwd = /tmp 
  
$ ~/path\ with\ space/test.sh 
basename = test.sh 
dirname = /home/Asus/path with space 
current pwd = /tmp 

Upvotes: 1

matt b
matt b

Reputation: 139991

Use dirname "$0":

test.sh:

#!/usr/bin/env bash

echo "The script you are running has:"
echo "basename: [$(basename "$0")]"
echo "dirname : [$(dirname "$0")]"
echo "pwd     : [$(pwd)]"

Using pwd alone will not work if you are not running the script from the directory it is contained in.

[~]$ pwd
/home/matt
[~]$ ./test.sh
The script you are running has:
basename: [test.sh]
dirname : [/home/matt]
pwd     : [/home/matt]

[~]$ cd /tmp
[~/tmp]$ ~/test.sh
The script you are running has:
basename: [test.sh]
dirname : [/home/matt]
pwd     : [/tmp]

Upvotes: 1306

midnite
midnite

Reputation: 5296

Why isn't any answer using the command -v "$0" solution?

The command -v "$0" approach is POSIX compliant.(1)

FYI, none of the following three commands are POSIX compliant.

  • realpath is not POSIX compliant and may not be available everywhere.(2)
  • readlink is also not POSIX compliant.(3)
  • which is not POSIX compliant, as mentioned.(1)

TL;DR: The POSIX compliant solution


if ${BASH_SOURCE[0]} else $(command -v)

  1. If ${BASH_SOURCE[0]} is available, use it.
  2. If ${BASH_SOURCE[0]} is not available, use command -v "$0".

This one-liner, which is POSIX compliant, returns the canonical file path of the script itself:

SELF_PATH=$( readlinkf "${BASH_SOURCE[0]:-"$(command -v -- "$0")"}" )

or using the non-POSIX complant readlink -f:

SELF_PATH=$( readlink -f "${BASH_SOURCE[0]:-"$(command -v -- "$0")"}" )

Analyse


The solution for finding the actual path of a script itself should be divided into two steps:

  1. Obtain a correct path of the script.
  2. Resolve the path.

While many methods combine both steps into one, they are, in fact, addressing different problems.

Once the correct path of the script is obtained, whether it is an absolute path, a relative path, or a symlink, there are several methods available to resolve it to the canonical path.

Test environment


Setup / Reproduce:

$ mkdir /tmp/test_step01 ; cd $_
$ mkdir bin some_dir other_dir
$ touch 'bin/prog in path' 'other_dir/source a script'
$ ln 'bin/prog in path' 'some_dir/prog outside'
$ ln -s '../some_dir/prog outside' other_dir/ln_prog_outside
$ chmod u+x 'bin/prog in path' 'other_dir/source a script'
$ tree /tmp/test_step01
/tmp/test_step01
├── bin
│   └── prog in path
├── other_dir
│   ├── ln_prog_outside -> ../some_dir/prog outside
│   └── source a script
└── some_dir
    └── prog outside

ls and cat:

$ ( cd /tmp/test_step01 ; ls -Uion 'bin/prog in path' 'some_dir/prog outside' other_dir/ln_prog_outside 'other_dir/source a script' )
192 -rwxr--r-x 2 1000 342 Dec 15 06:29 'bin/prog in path'
192 -rwxr--r-x 2 1000 342 Dec 15 06:29 'some_dir/prog outside'
194 lrwxrwxrwx 1 1000  24 Dec 15 01:39  other_dir/ln_prog_outside -> '../some_dir/prog outside'
193 -rwxr--r-- 1 1000  39 Dec 15 06:29 'other_dir/source a script'

$ cat '/tmp/test_step01/other_dir/source a script'
#!/bin/sh
. '../some_dir/prog outside'

Step 1 of 2: Obtain the correct path of the script


In the /tmp/test_step01/bin/prog in path (which is identical to /tmp/test_step01/some_dir/prog outside), these are the test codes trying to obtain the correct path of itself.

#!/bin/sh

echo -- Get its correct path ------------------------------

printf "\$0:             [%s]\n" "$0"
printf "pwd:            [%s]\n" "$(pwd)"
printf "which \$0:       [%s]\n" "$( which -- "$0" 2> /dev/null )"
printf "command -v \$0:  [%s]\n" "$( command -v -- "$0" )"
printf "BASH_SOURCE[0]: [%s]\n" "${BASH_SOURCE[0]}"

printf '\n'

The script will be called in the following different ways:

  • PATH="${PATH}:/tmp/test_step01/bin" 'prog in path'
  • PATH="${PATH}:/tmp/test_step01/bin" bash 'prog in path'. $0 loses path information.
  • '../some_dir/prog outside'. Call by relative path.
  • bash '../some_dir/prog outside'. Identical to the previous case.
  • ./ln_prog_outside runs a symlink to the program.
  • './source a script' runs a program that sources the target script.

Test calling the script in different ways.

Testing in bash:

$ ( cd /tmp/test_step01/other_dir ; PATH="${PATH}:/tmp/test_step01/bin" 'prog in path' ; PATH="${PATH}:/tmp/test_step01/bin" bash 'prog in path' ; '../some_dir/prog outside' ; bash '../some_dir/prog outside' ; ./ln_prog_outside ; './source a script' )
-- Get its correct path ------------------------------
$0:             [/tmp/test_step01/bin/prog in path]
pwd:            [/tmp/test_step01/other_dir]           #fail
which $0:       [/tmp/test_step01/bin/prog in path]
command -v $0:  [/tmp/test_step01/bin/prog in path]
BASH_SOURCE[0]: [/tmp/test_step01/bin/prog in path]

-- Get its correct path ------------------------------
$0:             [prog in path]                         #fail
pwd:            [/tmp/test_step01/other_dir]           #fail
which $0:       [/tmp/test_step01/bin/prog in path]
command -v $0:  [/tmp/test_step01/bin/prog in path]
BASH_SOURCE[0]: [/tmp/test_step01/bin/prog in path]

-- Get its correct path ------------------------------
$0:             [../some_dir/prog outside]
pwd:            [/tmp/test_step01/other_dir]           #fail
which $0:       [/tmp/test_step01/some_dir/prog outside]
command -v $0:  [../some_dir/prog outside]
BASH_SOURCE[0]: [../some_dir/prog outside]

-- Get its correct path ------------------------------
$0:             [../some_dir/prog outside]
pwd:            [/tmp/test_step01/other_dir]           #fail
which $0:       [/tmp/test_step01/some_dir/prog outside]
command -v $0:  [../some_dir/prog outside]
BASH_SOURCE[0]: [../some_dir/prog outside]

-- Get its correct path ------------------------------
$0:             [./ln_prog_outside]
pwd:            [/tmp/test_step01/other_dir]           #fail
which $0:       [/tmp/test_step01/other_dir/ln_prog_outside]
command -v $0:  [./ln_prog_outside]
BASH_SOURCE[0]: [./ln_prog_outside]

-- Get its correct path ------------------------------
$0:             [./source a script]                           #fail
pwd:            [/tmp/test_step01/other_dir]                  #fail
which $0:       [/tmp/test_step01/other_dir/source a script]  #fail
command -v $0:  [./source a script]                           #fail
BASH_SOURCE[0]: [../some_dir/prog outside]

In bash, ${BASH_SOURCE[0]} never fails in all cases.

Note that in some cases, ${BASH_SOURCE[0]} returns the relative path. It does not matter, as we will resolve the relative paths in step 2.

Change the hashbang from #!/bin/sh to #!/bin/zsh for files '/tmp/test_step01/some_dir/prog outside' and '/tmp/test_step01/other_dir/source a script'.

Test in zsh:

% ( cd /tmp/test_step01/other_dir ; PATH="${PATH}:/tmp/test_step01/bin" 'prog in path' ; PATH="${PATH}:/tmp/test_step01/bin" zsh -c 'prog\ in\ path' ; '../some_dir/prog outside' ; zsh '../some_dir/prog outside' ; ./ln_prog_outside ; './source a script' )
-- Get its correct path ------------------------------
$0:             [/tmp/test_step01/bin/prog in path]
pwd:            [/tmp/test_step01/other_dir]    # fail
which $0:       [/tmp/test_step01/bin/prog in path]
command -v $0:  [/tmp/test_step01/bin/prog in path]
BASH_SOURCE[0]: []                              # fail, not available

-- Get its correct path ------------------------------
$0:             [/tmp/test_step01/bin/prog in path]
pwd:            [/tmp/test_step01/other_dir]    # fail
which $0:       [/tmp/test_step01/bin/prog in path]
command -v $0:  [/tmp/test_step01/bin/prog in path]
BASH_SOURCE[0]: []                              # fail, not available

-- Get its correct path ------------------------------
$0:             [../some_dir/prog outside]
pwd:            [/tmp/test_step01/other_dir]    # fail
which $0:       [../some_dir/prog outside]
command -v $0:  [../some_dir/prog outside]
BASH_SOURCE[0]: []                              # fail, not available

-- Get its correct path ------------------------------
$0:             [../some_dir/prog outside]
pwd:            [/tmp/test_step01/other_dir]    # fail
which $0:       [../some_dir/prog outside]
command -v $0:  [../some_dir/prog outside]
BASH_SOURCE[0]: []                              # fail, not available

-- Get its correct path ------------------------------
$0:             [./ln_prog_outside]
pwd:            [/tmp/test_step01/other_dir]    # fail
which $0:       [./ln_prog_outside]
command -v $0:  [./ln_prog_outside]
BASH_SOURCE[0]: []                              # fail, not available

-- Get its correct path ------------------------------
$0:             [../some_dir/prog outside]
pwd:            [/tmp/test_step01/other_dir]    # fail
which $0:       [../some_dir/prog outside]
command -v $0:  [../some_dir/prog outside]
BASH_SOURCE[0]: []                              # fail, not available

In zsh, ${BASH_SOURCE[0]} is, of course, not available. Both $0 and command -v give the correct result. (Ignoring which as well.)

There are still some edge cases when the script is sourced directly from the environment rather than within a script.

In bash:

$ ( cd /tmp/test_step01/other_dir ; echo $SHELL ; . '../some_dir/prog outside' ; . ./ln_prog_outside ; . './source a script' )
/bin/bash
-- Get its correct path ------------------------------
$0:             [-bash]                        # fail
pwd:            [/tmp/test_step01/other_dir]   # fail
which $0:       []                             # fail, actually it returns 1
command -v $0:  []                             # fail, actually it returns 1
BASH_SOURCE[0]: [../some_dir/prog outside]

-- Get its correct path ------------------------------
$0:             [-bash]                        # fail
pwd:            [/tmp/test_step01/other_dir]   # fail
which $0:       []                             # fail, actually it returns 1
command -v $0:  []                             # fail, actually it returns 1
BASH_SOURCE[0]: [./ln_prog_outside]

-- Get its correct path ------------------------------
$0:             [-bash]                        # fail
pwd:            [/tmp/test_step01/other_dir]   # fail
which $0:       []                             # fail, actually it returns 1
command -v $0:  []                             # fail, actually it returns 1
BASH_SOURCE[0]: [../some_dir/prog outside]

In BASH, $BASH_SOURCE[0] still returns the correct values.

In zsh:

% ( cd /tmp/test_step01/other_dir ; echo $SHELL ; . '../some_dir/prog outside' ; . ./ln_prog_outside ; . './source a script' )
/bin/zsh
-- Get its correct path ------------------------------
$0:             [../some_dir/prog outside]
pwd:            [/tmp/test_step01/other_dir]   # fail
which $0:       [../some_dir/prog outside]
command -v $0:  [../some_dir/prog outside]
BASH_SOURCE[0]: []                             # fail, not available

-- Get its correct path ------------------------------
$0:             [./ln_prog_outside]
pwd:            [/tmp/test_step01/other_dir]   # fail
which $0:       [./ln_prog_outside]
command -v $0:  [./ln_prog_outside]
BASH_SOURCE[0]: []                             # fail, not available

-- Get its correct path ------------------------------
$0:             [../some_dir/prog outside]
pwd:            [/tmp/test_step01/other_dir]   # fail
which $0:       [../some_dir/prog outside]
command -v $0:  [../some_dir/prog outside]
BASH_SOURCE[0]: []                             # fail, not available

Of course ${BASH_SOURCE[0]} is not available in zsh. Do note that command -v $0 and even $0 return the correct path.

Verify our solution:

  1. If ${BASH_SOURCE[0]} is available, use it.
  2. If ${BASH_SOURCE[0]} is not available, use command -v "$0".

This "if BASH_SOURCE_0 else command-v" approach handles all the above cases well.

Important Note!


The successful of command -v "$0" depends on the value of "$0".

If you create a program with a name that is identical to an existing program, which you really should not do as it creates ambiguity, only one of the programs, either the one you newly created or the existing one, will be called.

Since both programs exist in $PATH, the one that appears earlier (on the left) in the $PATH string will be executed. command -v uses the same approach to find the path of the script file.

To illustrate this, let us consider an example where we create a file named /tmp/cryptsetup which has the same name as /sbin/cryptsetup:

$ command -v cryptsetup
/sbin/cryptsetup
$ cat /tmp/cryptsetup
#!/bin/sh
# /tmp/cryptsetup

( # script begins

echo -- 'HaHa! You are in my cryptsetup !!'
echo
printf "\$0:             [%s]\n" "$0"
printf "pwd:            [%s]\n" "$(pwd)"
printf "which \$0:       [%s]\n" "$( which -- "$0" 2> /dev/null )"
printf "command -v \$0:  [%s]\n" "$( command -v -- "$0" )"
printf "BASH_SOURCE[0]: [%s]\n" "${BASH_SOURCE[0]}"

) # script completes

Using the command PATH="${PATH}:/tmp" cryptsetup or PATH="${PATH}:/tmp" bash cryptsetup will execute the built-in /sbin/cryptsetup.

On the other hand, using the command PATH="/tmp:${PATH}" cryptsetup or PATH="/tmp:${PATH}" bash cryptsetup will execute our newly created /tmp/cryptsetup.

$ PATH="/tmp:${PATH}" cryptsetup
-- HaHa! You are in my cryptsetup !!

$0:             [/tmp/cryptsetup]
pwd:            [/tmp/test3]
which $0:       [/tmp/cryptsetup]
command -v $0:  [/tmp/cryptsetup]
BASH_SOURCE[0]: [/tmp/cryptsetup]
$ PATH="/tmp:${PATH}" bash cryptsetup
-- HaHa! You are in my cryptsetup !!

$0:             [cryptsetup]
pwd:            [/tmp/test3]
which $0:       [/tmp/cryptsetup]
command -v $0:  [/tmp/cryptsetup]
BASH_SOURCE[0]: [/tmp/cryptsetup]

In the second case above, "$0" resolves to the ambiguous name cryptsetup. However, command -v "$0" uses the same approach to search in the $PATH and returns the correct path for the intended program.

Step 2 of 2: Resolve the path.


Once we obtain a "correct" path, we proceed to canonicalize it. This could be easily done using the realpath or readlink -f commands. As we are joining the POSIX compliance club, we utilize only commands cd -P, ls -l and the $PWD variable.

This solution draws significant inspiration from the medium essay, and you can find similar code in the accepted answer of this thread.

Before moving on to the actual solution code, let us address a few issues. This will help clarify why a single cd -P may not be able to resolve the canonical path.

Setup the structure:

$ mkdir -p multi_level_dir/dir0 multi_level_file/dir{0,1,2} recursive
$ touch multi_level_dir/dir0/file0 multi_level_file/dir0/file0
$ ( cd multi_level_dir ; ln -s dir0 dir1 ; ln -s dir1 dir2 )
$ ( cd multi_level_file ; ln -s ../dir0/file0 dir1/file1 ; ln -s ../dir1/file1 dir2/file2 )
$ ln -s ../../multi_level_file/dir2/file2 multi_level_dir/dir0/lndir_lnfile
$ ( cd recursive ; ln -s file0 file1 ; ln -s file1 file2 ; ln -s file2 file0 )
$ tree
.
├── multi_level_dir
│   ├── dir0
│   │   ├── file0
│   │   └── lndir_lnfile -> ../../multi_level_file/dir2/file2
│   ├── dir1 -> dir0
│   └── dir2 -> dir1
├── multi_level_file
│   ├── dir0
│   │   └── file0
│   ├── dir1
│   │   └── file1 -> ../dir0/file0
│   └── dir2
│       └── file2 -> ../dir1/file1
└── recursive
    ├── file0 -> file2
    ├── file1 -> file0
    └── file2 -> file1

If the (script) file exists in a symlink to a directory, cd -P of dirname can return the canonical path successfully.

$ ( cd -P "$( dirname multi_level_dir/dir2/file0 )" ; printf "${PWD}\n" )
/tmp/test/multi_level_dir/dir0

However, if the directories are not symlinks but the (script) file itself is a symlink, cd -P cannot follow the symlink of the file because dirname returns its non-symlink directory. In this case, the command below will not provide the correct canonical path. The correct canonical path is /tmp/test/multi_level_file/dir0/file0.

$ ( cd -P "$( dirname multi_level_file/dir2/file2 )" ; printf "${PWD}\n" )
/tmp/test/multi_level_file/dir2

The accepted answer utilizes TARGET=$(readlink "$SOURCE") to resolve the file-symlink. I would suggest adding the -f option, so TARGET=$(readlink -f "$SOURCE") does not require additional loops. It is worth noting that readlink is not POSIX compliant. That is why the medium essay, as well as the code provided below, extract the link target from the output of ls -l.

Please note that in the medium essay, $max_symlinks is used to avoid an infinite loop in case of recursive symlinks. In the code below, I have made a change (improvement?) to check the inode number to avoid an infinite loop.

Note that readlink -f will not run into infinite loop.

$ readlink -f recursive/file2 ; echo $?
1

My proposed code to get the canonical path:

#!/bin/sh
# /home/midnite/bin/readlinkf

( # script begins

# -P : resolve symbolic link components
CDPATH='' cd -P . 2> /dev/null || exit 1

init_dir=$PWD   # remember the initial dir where this script is called.
RC=0

while [ $# -gt 0 ]; do

    # Watch out for $CDPATH gotchas
    # ref - https://bosker.wordpress.com/2012/02/12/bash-scripters-beware-of-the-cdpath/
    unset CDPATH        # to avoid changing to an unexpected directory

    # reset $PWD to the initial dir
    CDPATH='' cd -P "$init_dir" 2> /dev/null || { RC=1 ; break ; }

    target=$1

    # Use slash to distinguish dir/path or file
    #
    # Trim tailing slashes. They are meaningless.
    target=${target%"${target##*[!/]}"}
    #
    # If it is a directory, add a trailing slash.
    [ -d "${target:-/}" ] && target="${target}/"

    # inode is empty if $target does not exist.
    inode=$( stat --format '%i' "$target" 2> /dev/null )

    while true ; do
        if [ ! "$target" = "${target%/*}" ]; then   # contains '/' , target is a dir/path
            # cd -P resolves symlink(s) in the directory part
            case $target in
            ( /* )  CDPATH='' cd -P   "${target%/*}/" 2> /dev/null || { RC=1 ; break ; } ;; # abs path
            ( * )   CDPATH='' cd -P "./${target%/*}"  2> /dev/null || { RC=1 ; break ; } ;; # rel path
            esac
            target=${target##*/}    # basename
        fi

        ### Here: $PWD    is dirname
        ### Here: $target is basename only (won't be a dir)

        if [ -L "$target" ]; then   # if basename is a link
                        # extract $target from ls -l
            link=$(ls -l -- "$target" 2> /dev/null) || { RC=1 ; break ; }
            target=${link#*" $target -> "}
            [ "$(stat --format '%i' "$target" 2>&1)" = "$inode" ] && { RC=1 ; break ; }
                # if $target not found, stat error to stdout, != $inode, loop again
                # if $target = $inode, recursive encountered
        else                # basename is a file, or not exist at all
            target="${PWD%/}${target:+/}${target}"
            printf '%s\n' "${target:-/}"
            break               # success
        fi
    done

    shift
done

exit "$RC"

) # script completes

Test cases of resolvable symlinks:

$ readlinkf multi_level_dir/dir2/file0
/tmp/test/multi_level_dir/dir0/file0
$ readlinkf multi_level_file/dir2/file2
/tmp/test/multi_level_file/dir0/file0
$ readlinkf multi_level_dir/dir2/lndir_lnfile
/tmp/test/multi_level_file/dir0/file0

Test cases of recursive symlinks:

$ readlinkf recursive/file2 ; echo $?
1
$ readlink -f recursive/file2 ; echo $?
1

Test cases of dangling symlinks:

$ ln -s not_found dangling_file
$ ln -s dir/not_found dangling_dir
$ ls -lno dangling_*
lrwxrwxrwx 1 1000 13 Dec 15 02:33 dangling_dir -> dir/not_found
lrwxrwxrwx 1 1000  9 Dec 15 02:33 dangling_file -> not_found
$ readlinkf dangling_file
/tmp/test/not_found
$ readlink -f dangling_file
/tmp/test/not_found
$ readlinkf dangling_dir ; echo $?
1
$ readlink -f dangling_dir ; echo $?
1

Test cases of paths does not exist at all:

$ readlinkf no_such_file
/tmp/test/no_such_file
$ readlink -f no_such_file
/tmp/test/no_such_file
$ readlinkf no_such_dir/file ; echo $?
1
$ readlink -f no_such_dir/file ; echo $?
1

Test cases of multiple files, all successful:

$ readlinkf multi_level_dir/dir2/lndir_lnfile dangling_file ; echo $?
/tmp/test/multi_level_file/dir0/file0
/tmp/test/not_found
0
$ readlink -f multi_level_dir/dir2/lndir_lnfile dangling_file ; echo $?
/tmp/test/multi_level_file/dir0/file0
/tmp/test/not_found
0

Test cases of multiple files, at least one fails:

$ readlinkf multi_level_dir/dir2/lndir_lnfile no_such_dir/file dangling_file ; echo $?
/tmp/test/multi_level_file/dir0/file0
/tmp/test/not_found
1
$ readlink -f multi_level_dir/dir2/lndir_lnfile no_such_dir/file dangling_file ; echo $?
/tmp/test/multi_level_file/dir0/file0
/tmp/test/not_found
1

In all the above test cases, readlinkf behaves the same as readlink -f.

Final Solution


Copy the test environment:

$ cp -r /tmp/test_step{01,02}
$ ( cd /tmp/test_step02 ; ln -f 'bin/prog in path' 'some_dir/prog outside' )

Modify the file /tmp/test_step02/bin/prog in path:

#!/bin/sh

echo '======================================================'

printf "\$0:             [%s]\n" "$0"
printf "command -v \$0:  [%s]\n" "$( command -v -- "$0" )"
printf "BASH_SOURCE[0]: [%s]\n" "${BASH_SOURCE[0]}"

echo "-- if \$BASH_SOURCE[0] , else \$(command -v) -----------"

SELF_PATH=$( readlinkf "${BASH_SOURCE[0]:-"$(command -v -- "$0")"}" )

printf "SELF_PATH: [%s]\n" "$SELF_PATH"

Run the test for normal cases.

In bash:

$ ( cd /tmp/test_step02/other_dir ; PATH="${PATH}:/tmp/test_step02/bin" 'prog in path' ; PATH="${PATH}:/tmp/test_step02/bin" bash 'prog in path' ; '../some_dir/prog outside' ; bash '../some_dir/prog outside' ; ./ln_prog_outside ; './source a script' )
======================================================
$0:             [/tmp/test_step02/bin/prog in path]
command -v $0:  [/tmp/test_step02/bin/prog in path]
BASH_SOURCE[0]: [/tmp/test_step02/bin/prog in path]
-- if $BASH_SOURCE[0] , else $(command -v) -----------
SELF_PATH: [/tmp/test_step02/bin/prog in path]
======================================================
$0:             [prog in path]
command -v $0:  [/tmp/test_step02/bin/prog in path]
BASH_SOURCE[0]: [/tmp/test_step02/bin/prog in path]
-- if $BASH_SOURCE[0] , else $(command -v) -----------
SELF_PATH: [/tmp/test_step02/bin/prog in path]
======================================================
$0:             [../some_dir/prog outside]
command -v $0:  [../some_dir/prog outside]
BASH_SOURCE[0]: [../some_dir/prog outside]
-- if $BASH_SOURCE[0] , else $(command -v) -----------
SELF_PATH: [/tmp/test_step02/some_dir/prog outside]
======================================================
$0:             [../some_dir/prog outside]
command -v $0:  [../some_dir/prog outside]
BASH_SOURCE[0]: [../some_dir/prog outside]
-- if $BASH_SOURCE[0] , else $(command -v) -----------
SELF_PATH: [/tmp/test_step02/some_dir/prog outside]
======================================================
$0:             [./ln_prog_outside]
command -v $0:  [./ln_prog_outside]
BASH_SOURCE[0]: [./ln_prog_outside]
-- if $BASH_SOURCE[0] , else $(command -v) -----------
SELF_PATH: [/tmp/test_step02/some_dir/prog outside]
======================================================
$0:             [./source a script]
command -v $0:  [./source a script]
BASH_SOURCE[0]: [../some_dir/prog outside]
-- if $BASH_SOURCE[0] , else $(command -v) -----------
SELF_PATH: [/tmp/test_step02/some_dir/prog outside]

Modidy the hashbangs. Test again in zsh:

% ( cd /tmp/test_step02/other_dir ; PATH="${PATH}:/tmp/test_step02/bin" 'prog in path' ; PATH="${PATH}:/tmp/test_step02/bin" zsh -c 'prog\ in\ path' ; '../some_dir/prog outside' ; zsh '../some_dir/prog outside' ; ./ln_prog_outside ; './source a script' )
======================================================
$0:             [/tmp/test_step02/bin/prog in path]
command -v $0:  [/tmp/test_step02/bin/prog in path]
BASH_SOURCE[0]: []
-- if $BASH_SOURCE[0] , else $(command -v) -----------
SELF_PATH: [/tmp/test_step02/bin/prog in path]
======================================================
$0:             [/tmp/test_step02/bin/prog in path]
command -v $0:  [/tmp/test_step02/bin/prog in path]
BASH_SOURCE[0]: []
-- if $BASH_SOURCE[0] , else $(command -v) -----------
SELF_PATH: [/tmp/test_step02/bin/prog in path]
======================================================
$0:             [../some_dir/prog outside]
command -v $0:  [../some_dir/prog outside]
BASH_SOURCE[0]: []
-- if $BASH_SOURCE[0] , else $(command -v) -----------
SELF_PATH: [/tmp/test_step02/some_dir/prog outside]
======================================================
$0:             [../some_dir/prog outside]
command -v $0:  [../some_dir/prog outside]
BASH_SOURCE[0]: []
-- if $BASH_SOURCE[0] , else $(command -v) -----------
SELF_PATH: [/tmp/test_step02/some_dir/prog outside]
======================================================
$0:             [./ln_prog_outside]
command -v $0:  [./ln_prog_outside]
BASH_SOURCE[0]: []
-- if $BASH_SOURCE[0] , else $(command -v) -----------
SELF_PATH: [/tmp/test_step02/some_dir/prog outside]
======================================================
$0:             [../some_dir/prog outside]
command -v $0:  [../some_dir/prog outside]
BASH_SOURCE[0]: []
-- if $BASH_SOURCE[0] , else $(command -v) -----------
SELF_PATH: [/tmp/test_step02/some_dir/prog outside]

Run the test for edge cases where the script is sourced directly from the environment:

In bash:

$ ( cd /tmp/test_step02/other_dir ; echo $SHELL ; . '../some_dir/prog outside' ; . ./ln_prog_outside ; . './source a script' )
/bin/bash
======================================================
$0:             [-bash]
command -v $0:  []
BASH_SOURCE[0]: [../some_dir/prog outside]
-- if $BASH_SOURCE[0] , else $(command -v) -----------
SELF_PATH: [/tmp/test_step02/some_dir/prog outside]
======================================================
$0:             [-bash]
command -v $0:  []
BASH_SOURCE[0]: [./ln_prog_outside]
-- if $BASH_SOURCE[0] , else $(command -v) -----------
SELF_PATH: [/tmp/test_step02/some_dir/prog outside]
======================================================
$0:             [-bash]
command -v $0:  []
BASH_SOURCE[0]: [../some_dir/prog outside]
-- if $BASH_SOURCE[0] , else $(command -v) -----------
SELF_PATH: [/tmp/test_step02/some_dir/prog outside]

In zsh:

% ( cd /tmp/test_step02/other_dir ; echo $SHELL ; . '../some_dir/prog outside' ; . ./ln_prog_outside ; . './source a script' )
/bin/zsh
======================================================
$0:             [../some_dir/prog outside]
command -v $0:  [../some_dir/prog outside]
BASH_SOURCE[0]: []
-- if $BASH_SOURCE[0] , else $(command -v) -----------
SELF_PATH: [/tmp/test_step02/some_dir/prog outside]
======================================================
$0:             [./ln_prog_outside]
command -v $0:  [./ln_prog_outside]
BASH_SOURCE[0]: []
-- if $BASH_SOURCE[0] , else $(command -v) -----------
SELF_PATH: [/tmp/test_step02/some_dir/prog outside]
======================================================
$0:             [../some_dir/prog outside]
command -v $0:  [../some_dir/prog outside]
BASH_SOURCE[0]: []
-- if $BASH_SOURCE[0] , else $(command -v) -----------
SELF_PATH: [/tmp/test_step02/some_dir/prog outside]

All the test cases above produce the correct results.

Solution: Explicit Recap


This one-liner, which is POSIX compliant, returns the canonical file path of the script itself:

SELF_PATH=$( readlinkf "${BASH_SOURCE[0]:-"$(command -v -- "$0")"}" )

given that the program readlinkf is implemented in the system. (Please refer to the code above. Search for /home/midnite/bin/readlinkf.)

If it is certain that the target system supports readlink -f, which is not a POSIX requirement,(3) it is safe to change readlinkf to readlink -f, and the one-liner above will produce the same correct results.

Remarks

  1. Credit to https://hynek.me/til/which-not-posix/
  2. What's the difference between "realpath" and "readlink -f", first answer, https://unix.stackexchange.com/a/136527/150246
  3. https://medium.com/mkdir-awesome/posix-alternatives-for-readlink-21a4bfe0455c

Upvotes: 5

Andry
Andry

Reputation: 2727

There is no 100% portable and reliable way to request a path to a current script directory. Especially between different backends like Cygwin, MinGW, MSYS, Linux, etc. This issue was not properly and completely resolved in Bash for ages.

For example, this could not be resolved if you want to request the path after the source command to make nested inclusion of another Bash script which is in turn use the same source command to include another Bash script and so on.

In case of the source command, I suggest to replace the source command with something like this:

function include()
{
  if [[ -n "$CURRENT_SCRIPT_DIR" ]]; then
    local dir_path=... get directory from `CURRENT_SCRIPT_DIR/$1`, depends if $1 is absolute path or relative ...
    local include_file_path=...
  else
    local dir_path=... request the directory from the "$1" argument using one of answered here methods...
    local include_file_path=...
  fi
  ... push $CURRENT_SCRIPT_DIR in to stack ...
  export CURRENT_SCRIPT_DIR=... export current script directory using $dir_path ...
  source "$include_file_path"
  ... pop $CURRENT_SCRIPT_DIR from stack ...
}

From now on, the use of include(...) is based on previous CURRENT_SCRIPT_DIR in your script.

This only works when you can replace all source commands by include command. If you can't, then you have no choice. At least until developers of the Bash interpreter make an explicit command to request the current running script directory path.

My own closest implementation to this:
https://github.com/andry81/tacklelib/tree/HEAD/bash/tacklelib/bash_tacklelib

(search for the tkl_include function)

Upvotes: -4

digory doo
digory doo

Reputation: 2311

Yet another variant:

SELF=$(SELF=$(dirname "$0") && bash -c "cd \"$SELF\" && pwd")
echo "$SELF"

This works on macOS as well, determines the canonical path, and does not change the current directory.

Upvotes: 6

TG Gowda
TG Gowda

Reputation: 11957

Here is an easy-to-remember script:

DIR="$( dirname -- "${BASH_SOURCE[0]}"; )";   # Get the directory name
DIR="$( realpath -e -- "$DIR"; )";    # Resolve its full path if need be

Upvotes: 155

dogbane
dogbane

Reputation: 274758

#!/usr/bin/env bash

SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )

is a useful one-liner which will give you the full directory name of the script no matter where it is being called from.

It will work as long as the last component of the path used to find the script is not a symlink (directory links are OK). If you also want to resolve any links to the script itself, you need a multi-line solution:

#!/usr/bin/env bash

SOURCE=${BASH_SOURCE[0]}
while [ -L "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink
  DIR=$( cd -P "$( dirname "$SOURCE" )" >/dev/null 2>&1 && 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 2>&1 && pwd )

This last one will work with any combination of aliases, source, bash -c, symlinks, etc.

Beware: if you cd to a different directory before running this snippet, the result may be incorrect!

Also, watch out for $CDPATH gotchas, and stderr output side effects if the user has smartly overridden cd to redirect output to stderr instead (including escape sequences, such as when calling update_terminal_cwd >&2 on Mac). Adding >/dev/null 2>&1 at the end of your cd command will take care of both possibilities.

To understand how it works, try running this more verbose form:

#!/usr/bin/env bash

SOURCE=${BASH_SOURCE[0]}
while [ -L "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink
  TARGET=$(readlink "$SOURCE")
  if [[ $TARGET == /* ]]; then
    echo "SOURCE '$SOURCE' is an absolute symlink to '$TARGET'"
    SOURCE=$TARGET
  else
    DIR=$( dirname "$SOURCE" )
    echo "SOURCE '$SOURCE' is a relative symlink to '$TARGET' (relative to '$DIR')"
    SOURCE=$DIR/$TARGET # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located
  fi
done
echo "SOURCE is '$SOURCE'"
RDIR=$( dirname "$SOURCE" )
DIR=$( cd -P "$( dirname "$SOURCE" )" >/dev/null 2>&1 && pwd )
if [ "$DIR" != "$RDIR" ]; then
  echo "DIR '$RDIR' resolves to '$DIR'"
fi
echo "DIR is '$DIR'"

And it will print something like:

SOURCE './scriptdir.sh' is a relative symlink to 'sym2/scriptdir.sh' (relative to '.')
SOURCE is './sym2/scriptdir.sh'
DIR './sym2' resolves to '/home/ubuntu/dotfiles/fo fo/real/real1/real2'
DIR is '/home/ubuntu/dotfiles/fo fo/real/real1/real2'

Upvotes: 8207

phatblat
phatblat

Reputation: 4070

The dirname command is the most basic, simply parsing the path up to the filename off of the $0 (script name) variable:

dirname -- "$0";

But, as matt b pointed out, the path returned is different depending on how the script is called. pwd doesn't do the job because that only tells you what the current directory is, not what directory the script resides in. Additionally, if a symbolic link to a script is executed, you're going to get a (probably relative) path to where the link resides, not the actual script.

Some others have mentioned the readlink command, but at its simplest, you can use:

dirname -- "$( readlink -f -- "$0"; )";

readlink will resolve the script path to an absolute path from the root of the filesystem. So, any paths containing single or double dots, tildes and/or symbolic links will be resolved to a full path.

Here's a script demonstrating each of these, whatdir.sh:

#!/usr/bin/env bash

echo "pwd: `pwd`"
echo "\$0: $0"
echo "basename: `basename -- "$0"`"
echo "dirname: `dirname -- "$0"`"
echo "dirname/readlink: $( dirname -- "$( readlink -f -- "$0"; )"; )"

Running this script in my home dir, using a relative path:

>>>$ ./whatdir.sh
pwd: /Users/phatblat
$0: ./whatdir.sh
basename: whatdir.sh
dirname: .
dirname/readlink: /Users/phatblat

Again, but using the full path to the script:

>>>$ /Users/phatblat/whatdir.sh
pwd: /Users/phatblat
$0: /Users/phatblat/whatdir.sh
basename: whatdir.sh
dirname: /Users/phatblat
dirname/readlink: /Users/phatblat

Now changing directories:

>>>$ cd /tmp
>>>$ ~/whatdir.sh
pwd: /tmp
$0: /Users/phatblat/whatdir.sh
basename: whatdir.sh
dirname: /Users/phatblat
dirname/readlink: /Users/phatblat

And finally using a symbolic link to execute the script:

>>>$ ln -s ~/whatdir.sh whatdirlink.sh
>>>$ ./whatdirlink.sh
pwd: /tmp
$0: ./whatdirlink.sh
basename: whatdirlink.sh
dirname: .
dirname/readlink: /Users/phatblat

There is however one case where this doesn't work, when the script is sourced (instead of executed) in bash:

>>>$ cd /tmp
>>>$ . ~/whatdir.sh  
pwd: /tmp
$0: bash
basename: bash
dirname: .
dirname/readlink: /tmp

Upvotes: 695

Simon Rigét
Simon Rigét

Reputation: 2905

This should do it:

DIR="$(dirname "$(realpath "$0")")"

This works with symlinks and spaces in path.

Please see the man pages for dirname and realpath.

Please add a comment on how to support MacOS. I'm sorry I can verify it.

Upvotes: 123

SpoonMeiser
SpoonMeiser

Reputation: 20457

pwd can be used to find the current working directory, and dirname to find the directory of a particular file (command that was run, is $0, so dirname $0 should give you the directory of the current script).

However, dirname gives precisely the directory portion of the filename, which more likely than not is going to be relative to the current working directory. If your script needs to change directory for some reason, then the output from dirname becomes meaningless.

I suggest the following:

#!/usr/bin/env bash

reldir="$( dirname -- "$0"; )";
cd "$reldir";
directory="$( pwd; )";

echo "Directory is ${directory}";

This way, you get an absolute, rather than a relative directory.

Since the script will be run in a separate Bash instance, there isn't any need to restore the working directory afterwards, but if you do want to change back in your script for some reason, you can easily assign the value of pwd to a variable before you change directory, for future use.

Although just

cd "$( dirname -- "$0"; )";

solves the specific scenario in the question, I find having the absolute path to more more useful generally.

Upvotes: 76

Fabien
Fabien

Reputation: 7039

Short answer:

"`dirname -- "$0";`"

or (preferably):

"$( dirname -- "$0"; )"

Upvotes: 127

Mr Shark
Mr Shark

Reputation: 26478

You can use $BASH_SOURCE:

#!/usr/bin/env bash

scriptdir="$( dirname -- "$BASH_SOURCE"; )";

Note that you need to use #!/bin/bash and not #!/bin/sh since it's a Bash extension.

Upvotes: 146

user25866
user25866

Reputation: 301

pushd . > '/dev/null';
SCRIPT_PATH="${BASH_SOURCE[0]:-$0}";

while [ -h "$SCRIPT_PATH" ];
do
    cd "$( dirname -- "$SCRIPT_PATH"; )";
    SCRIPT_PATH="$( readlink -f -- "$SCRIPT_PATH"; )";
done

cd "$( dirname -- "$SCRIPT_PATH"; )" > '/dev/null';
SCRIPT_PATH="$( pwd; )";
popd  > '/dev/null';

It works for all versions, including

  • when called via multiple depth soft link,
  • when the file it
  • when script called by command "source" aka . (dot) operator.
  • when arg $0 is modified from caller.
  • "./script"
  • "/full/path/to/script"
  • "/some/path/../../another/path/script"
  • "./some/folder/script"

Alternatively, if the Bash script itself is a relative symlink you want to follow it and return the full path of the linked-to script:

pushd . > '/dev/null';
SCRIPT_PATH="${BASH_SOURCE[0]:-$0}";

while [ -h "$SCRIPT_PATH" ];
do
    cd "$( dirname -- "$SCRIPT_PATH"; )";
    SCRIPT_PATH="$( readlink -f -- "$SCRIPT_PATH"; )";
done

cd "$( dirname -- "$SCRIPT_PATH"; )" > '/dev/null';
SCRIPT_PATH="$( pwd; )";
popd  > '/dev/null';

SCRIPT_PATH is given in full path, no matter how it is called.

Just make sure you locate this at start of the script.

Upvotes: 206

Alfred.37
Alfred.37

Reputation: 159

You can get the source directory of a Bash script from within the script itself on follow short way:

script_path=$(dirname "$(readlink -f "$0")")"/"
echo "$script_path"

Sample output:

/home/username/desktop/

Upvotes: 0

M Imam Pratama
M Imam Pratama

Reputation: 1311

If not sourced by parent script and not symlinked, $0 is enough:

script_path="$0"

If sourced by parent script and not symlinked, use $BASH_SOURCE or ${BASH_SOURCE[0]}:

script_path="$BASH_SOURCE"

If symlinked, use $BASH_SOURCE with realpath or readlink -f to get the real file path:

script_path="$(realpath "$BASH_SOURCE")"

In addition, realpath or readlink -f returns the absolute path.

To get the directory of the script, use dirname:

script_directory="$(dirname "$script_path")"

Note

Upvotes: 2

mrucci
mrucci

Reputation: 4470

This is, annoyingly, the only one-liner I've found that works on both Linux and macOS when the executable script is a symlink:

SCRIPT_DIR=$(python -c "import os; print(os.path.dirname(os.path.realpath('${BASH_SOURCE[0]}')))")

or, similarly, using python3 pathlib module:

SCRIPT_DIR=$(python3 -c "from pathlib import Path; print(Path('${BASH_SOURCE[0]}').resolve().parent)")

Tested on Linux and macOS and compared to other solutions in this gist: https://gist.github.com/ptc-mrucci/61772387878ed53a6c717d51a21d9371

Upvotes: 8

DarkPark
DarkPark

Reputation: 1

Try using:

real=$(realpath "$(dirname "$0")")

Upvotes: 20

Rawstring
Rawstring

Reputation: 67

This is how I work it on my scripts:

pathvar="$( cd "$( dirname $0 )" && pwd )"

This will tell you which directory the Launcher (current script) is being executed from.

Upvotes: 2

Alexander Stohr
Alexander Stohr

Reputation: 326

The top response does not work in all cases...

As I had problems with the BASH_SOURCE with the included 'cd' approach on some very fresh and also on less fresh installed Ubuntu 16.04 (Xenial Xerus) systems when invoking the shell script by means of "sh my_script.sh", I tried out something different that as of now seems to run quite smoothly for my purposes. The approach is a bit more compact in the script and is further much lesser cryptic feeling.

This alternate approach uses the external applications 'realpath' and 'dirname' from the coreutils package. (Okay, not anyone likes the overhead of invoking secondary processes - but when seeing the multi-line scripting for resolving the true object it won't be that bad either having it solve in a single binary usage.)

So let’s see one example of those alternate solution for the described task of querying the true absolute path to a certain file:

PATH_TO_SCRIPT=`realpath -s $0`
PATH_TO_SCRIPT_DIR=`dirname $PATH_TO_SCRIPT`

But preferably you should use this evolved version to also support the use of paths with spaces (or maybe even some other special characters):

PATH_TO_SCRIPT=`realpath -s "$0"`
PATH_TO_SCRIPT_DIR=`dirname "$PATH_TO_SCRIPT"`

Indeed, if you don’t need the value of the SCRIPT variable then you might be able to merge this two-liner into even a single line. But why really shall you spend the effort for this?

Upvotes: 7

Binary Phile
Binary Phile

Reputation: 2668

Most answers either don't handle files which are symlinked via a relative path, aren't one-liners or don't handle BSD (Mac). A solution which does all three is:

HERE=$(cd "$(dirname "$BASH_SOURCE")"; cd -P "$(dirname "$(readlink "$BASH_SOURCE" || echo .)")"; pwd)

First, cd to bash's conception of the script's directory. Then readlink the file to see if it is a symlink (relative or otherwise), and if so, cd to that directory. If not, cd to the current directory (necessary to keep things a one-liner). Then echo the current directory via pwd.

You could add -- to the arguments of cd and readlink to avoid issues of directories named like options, but I don't bother for most purposes.

You can see the full explanation with illustrations here:

https://www.binaryphile.com/bash/2020/01/12/determining-the-location-of-your-script-in-bash.html

Upvotes: 6

l0b0
l0b0

Reputation: 58918

None of the current solutions work if there are any newlines at the end of the directory name - They will be stripped by the command substitution. To work around this you can append a non-newline character inside the command substitution and then strip just that character off:

dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd && echo x)"
dir="${dir%x}"

This protects against two very common situations: Accidents and sabotage. A script shouldn't fail in unpredictable ways just because someone, somewhere, did a mkdir $'\n'.

Upvotes: 7

Top-Master
Top-Master

Reputation: 8816

The below stores the script's directory path in the dir variable.

(It also tries to support being executed under Cygwin in Windows.)

And at last it runs the my-sample-app executable with all arguments passed to this script using "$@":

#!/usr/bin/env sh

dir=$(cd "${0%[/\\]*}" > /dev/null && pwd)

if [ -d /proc/cygdrive ]; then
    case "$(uname -s)" in
        CYGWIN*|MINGW32*|MSYS*|MINGW*)
            # We are under Windows, so translate path to Windows format.
            dir=$(cygpath -m "$dir");
            ;;
    esac
fi

# Runs the executable which is beside this script
"${dir}/my-sample-app" "$@"

Upvotes: 0

ghchoi
ghchoi

Reputation: 5156

I tried the followings with 3 different executions.

echo $(realpath $_)

. application         # /correct/path/to/dir or /path/to/temporary_dir
bash application      # /path/to/bash
/PATH/TO/application  # /correct/path/to/dir

echo $(realpath $(dirname $0))

. application         # failed with `realpath: missing operand`
bash application      # /correct/path/to/dir
/PATH/TO/application  # /correct/path/to/dir

echo $(realpath $BASH_SOURCE)

$BASH_SOURCE is basically the same with ${BASH_SOURCE[0]}.

. application         # /correct/path/to/dir
bash application      # /correct/path/to/dir
/PATH/TO/application  # /correct/path/to/dir

Only $(realpath $BASH_SOURCE) seems to be reliable.

Upvotes: 2

Thomas Guyot-Sionnest
Thomas Guyot-Sionnest

Reputation: 2510

One advantage of this method is that it doesn't involve anything outside Bash itself and does not fork any subshell neither.

First, use pattern substitution to replace anything not starting with / (i.e., a relative path) with $PWD/. Since we use a substitution to match the first character of $0, we also have to append it back (${0:0:1} in the substitution).

Now we have a full path to the script; we can get the directory by removing the last / and anything the follows (i.e., the script name). That directory can then be used in cd or as a prefix to other paths relative to your script.

#!/bin/bash

BIN=${0/#[!\/]/"$PWD/${0:0:1}"}
DIR=${BIN%/*}

cd "$DIR"

If your script may be sourced rather than executed, you can of course replace $0 with ${BASH_SOURCE[0]}, such as:

BIN=${BASH_SOURCE[0]/#[!\/]/"$PWD/${BASH_SOURCE[0]:0:1}"}

This will work for executable scripts too. It's longer, but more polyvalent.

Upvotes: 5

todd_dsm
todd_dsm

Reputation: 1076

I think the simplest answer is a parameter expansion of the original variable:

#!/usr/bin/env bash

DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
echo "opt1; original answer: $DIR"
echo ''

echo "opt2; simple answer  : ${BASH_SOURCE[0]%/*}"

It should produce output like:

$ /var/tmp/test.sh
opt1; original answer: /var/tmp

opt2; simple answer  : /var/tmp

The variable/parameter expansion ${BASH_SOURCE[0]%/*}" seems much easier to maintain.

Upvotes: 0

Related Questions