5

I can use the external command realpath to get the absolute path of a file:

realpath tmp/toto

returns

/home/john/tmp/toto

Can I use a bash built-in to get the same effect?

Kusalananda
  • 333,661
Philippe
  • 1,435
  • Why do you want a bash builtin cmd for that ? Any specific reason that may enlighten us ? – Cbhihe Dec 21 '19 at 11:30
  • 2
    @Cbhihe On non-Linux systems, there may not be a realpath command available at all. – Kusalananda Dec 21 '19 at 11:40
  • On several non-Linux operating systems there is such a command, because they invented this stuff. FreeBSD version 4.3 in 2001 was the origin of realpath, only in GNU coreutils since 2012, for example. And even if there were not, there is no requirement for a built-in command. An alternative external command would surely suffice. OpenBSD version 2.1 in 1997 was the origin of readlink -f, for example. https://unix.stackexchange.com/a/24342/5132 – JdeBP Dec 21 '19 at 14:15
  • @JdeBP, if there was a built-in command (as in zsh), that would avoid spawning an OS process to just get absolute path. – Philippe Dec 21 '19 at 14:43
  • Which is an argument that you could make for a text editor, and moreover is not the reason that Kusalananda suggested. – JdeBP Dec 21 '19 at 18:25
  • bash will spawn a separate OS process for any subshell anyway, and unless your mythical realpath builtin were specially designed to take a variable name (like export, local, etc), you would need a command substitution (= subshell -> subprocess) in order to get its output into a variable. –  Dec 22 '19 at 05:41
  • My concern is mainly to avoid sub-shell and external command. I will stick with realpath for now, to keep it simple. – Philippe Dec 22 '19 at 17:02
  • If one answer provided a solution for you, you should accept that answer. See e.g. https://unix.stackexchange.com/help/someone-answers – Kusalananda Mar 06 '20 at 10:55

4 Answers4

5

The bash shell does not have a built-in way to explicitly get an absolute path given a relative pathname.

In the zsh shell, one could do

pathname=../../home/kk/.zshenv
print -r -- $pathname:P

and get /home/kk/.zshenv back (note that it also resolves all symbolic links wherever possible is a similar way as realpath() would).

In bash, you could, for a pathname designating a non-directory and assuming you have search access to its parent directory, use

$ pathname=../../home/kk/.zshenv
$ ( OLDPWD=- CDPATH= cd -P -- "${pathname%/*}" && printf '%s/%s\n' "$PWD" "${pathname##*/}" )
/home/kk/.zshenv

That is, temporarily (in a sub-shell) cd to the directory holding the file, and then construct the absolute pathname using the value $PWD and the filename component of the pathname. The -P is needed to avoid cd's special treatment of .. components. It has the side effect of resolving symlinks components in the directory itself. If the file itself is a symlink however, it won't be resolved (that's a difference from zsh's :P modifier).

We set OLDPWD to - to work around the fact that cd -P -- - does a chdir($OLDPWD) instead of chdir("-") (that trick works in bash but not all other sh implementations).

For a directory path it's easier:

$ pathname=../../home/kk
$ ( OLDPWD=- CDPATH= cd -P -- "$pathname" && pwd )
/home/kk

This could obviously be put into a shell function for convenience:

myrealpath () (
    if [[ -d $1 ]]; then
        OLDPWD=- CDPATH= cd -P -- "$1" && pwd
    else
        OLDPWD=- CDPATH= cd -P -- "${1%/*}" && printf '%s/%s\n' "$PWD" "${1##*/}"
    fi
)

(Note that this whole function is defined in a sub-shell to avoid changing the working directory for the calling shell.)

Testing this function:

$ myrealpath ../../home/kk
/home/kk
$ myrealpath ../../home/kk/.zshenv
/home/kk/.zshenv

Depending on the OS, using the function with no argument will return the absolute path of the current directory or give an error. In future versions of bash, that's likely to be an error on every system, as cd '' to fail will become a POSIX requirement.

Kusalananda
  • 333,661
  • Thanks for detailed answer !

    I'm aware of $pathname(:a) in zsh. That's exactly what I want to find equivalent in bash.

    Using a sub-shell to get just an absolute path is a little overkill.

    myrealpath is a nice try, but it does "cd", I still have to "cd" back to the original directory.

    – Philippe Dec 21 '19 at 11:49
  • @Philippe The point of the subshell is that you don't actually have to change back to the original directory. The working directory is local to the subshell. Using (cd somepath && some-command) is a very common way of running a command with a displaced working directory. It avoids having to cd back and forth in complicated ways. – Kusalananda Dec 21 '19 at 12:01
  • Fair point. I think I'll use realpath for now, to keep it simple. – Philippe Dec 21 '19 at 12:18
  • In zsh, that's be more like $pathname:P, $pathname:a, $pathname:A. See the manual (info zsh Modifiers) for the differences. – Stéphane Chazelas Dec 21 '19 at 12:44
  • @StéphaneChazelas As the question is geared towards bash, I have deleted the mentioning of :A and will instead only mention :P, and will not go down the path (!) of trying to explain more about that. – Kusalananda Dec 21 '19 at 13:54
  • Since op already has zsh installed: realpath(){ zsh -c "realpath $@"; } laziest – Rakib Fiha Dec 21 '19 at 14:04
  • @RakibFiha I'm noting that their motivation is performance (this was mentioned in a comment from them to a now deleted answer), which means that starting up a separate shell instance might not be the best option. Also, the zsh shell does not have a built in realpath utility, so your function would end up calling the external realpath utility anyway, if it's available. – Kusalananda Dec 21 '19 at 14:07
  • Apologies, from the question, I thought realpath was zsh bultin or something like that. Just saw, OP mentioned its an external command – Rakib Fiha Dec 21 '19 at 14:10
  • @RakibFiha No worries :-) No, it's just a utility commonly found on Linux systems. – Kusalananda Dec 21 '19 at 14:11
  • I meant $pathname:P as the P modifier applied to parameter expansion as opposed to a (:P) glob qualifier. The glob qualifier requires the file to exist. – Stéphane Chazelas Dec 21 '19 at 17:47
  • @StéphaneChazelas Duh. Thanks. – Kusalananda Dec 21 '19 at 18:53
  • myrealpath fails to produce the same results as realpath if the last path component is a symlink. Also, if you just want an absolute path, I don't understand the need to use cd. Just using $PWD should be sufficient (it's a different story if you use cd -P). – jrw32982 Dec 27 '19 at 06:01
1

Bash comes with a realpath loadable extension. Since bash-4.4 this should be installed by default¹, so you should be able to do:

BASH_LOADABLES_PATH=${BASH_LOADABLES_PATH:-/usr/local/lib/bash:/usr/lib/bash}
enable -f realpath realpath
realpath ..
help realpath

Once enabled, each loadable command becomes a "real" builtin with no additional fork/exec overhead, and with the ability to manipulate the bash environment (where offered, e.g. the mypid loadable)..

The -f option specifies the path to the loadable extension binary, if it's not in the expected place or your distro didn't set it correctly, you will need to set the variable BASH_LOADABLES_PATH to the directory containing extensions (or, use -f with the full path to the loadable). If it's set correctly, you won't need the first line above.

When writing portable scripts that prefer, but do not require such extensions, I will use a wrapper function anyway — along the lines of @Kusalananda's — so that it can fall back to an external (such as readlink or realpath from coreutils).


  1. This extension is available since bash-2.05, but prior to bash-4.4 extensions were not compiled and installed by default, though it is straightforward to do so if you have a working build environment (C compiler etc.). Distributions may or may not include the extensions, sadly RHEL 8 (and hence CentOS 8) build bash-4.4 with the loadable extensions deliberately removed.
mr.spuratic
  • 9,901
  • Thank you so much! – Philippe Dec 23 '19 at 13:36
  • Any pointers for how to build these extensions? I'm running into trouble, despite the instructions in the wiki article you link to: https://unix.stackexchange.com/q/582361/19157 – dimo414 Apr 25 '20 at 02:30
0

I suggest the following, which have a high probability of working on non-Linux OSes (including AIX). To compute an absolute path (i.e. a path starting at /):

abspath() {
  [[ $1 = /* ]] && printf "%s\n" "$1" || printf "%s\n" "$PWD/$1"
}

For a canonical path (i.e. one with all symlinks expanded):

canonical_path() {
  perl -mCwd -le ' print Cwd::abs_path $ARGV[0] ' -- "$1"
}

When testing against other possible solutions (including a call to realpath), you'll notice a significant difference depending on whether or not the last path component exists, and whether or not the last path component is a symlink.

jrw32982
  • 723
-1

Some systems possess "realpath" command, e.g. Ubuntu. You can try realpath ~/foo.txt and it could give something like /home/user/foo.txt, then you can use basedir to cut off file name.