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)
- If
${BASH_SOURCE[0]}
is available, use it.
- 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:
- Obtain a correct path of the script.
- 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:
- If
${BASH_SOURCE[0]}
is available, use it.
- 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
- Credit to https://hynek.me/til/which-not-posix/
- What's the difference between "realpath" and "readlink -f", first answer, https://unix.stackexchange.com/a/136527/150246
- https://medium.com/mkdir-awesome/posix-alternatives-for-readlink-21a4bfe0455c
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd && echo x)"
- and remove it without a command substitution -DIR="${DIR%x}"
. – Japheticmkdir $'\n'
. – Japhetic