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?

8
  • Why do you want a bash builtin cmd for that ? Any specific reason that may enlighten us ?
    – Cbhihe
    Dec 21, 2019 at 11:30
  • 2
    @Cbhihe On non-Linux systems, there may not be a realpath command available at all.
    – Kusalananda
    Dec 21, 2019 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. unix.stackexchange.com/a/24342/5132
    – JdeBP
    Dec 21, 2019 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, 2019 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, 2019 at 18:25

4 Answers 4

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.

12
  • 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, 2019 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, 2019 at 12:01
  • Fair point. I think I'll use realpath for now, to keep it simple.
    – Philippe
    Dec 21, 2019 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. Dec 21, 2019 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, 2019 at 13:54
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.
2
  • Thank you so much!
    – Philippe
    Dec 23, 2019 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: unix.stackexchange.com/q/582361/19157
    – dimo414
    Apr 25, 2020 at 2: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.

-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.

2
  • realpath is an external command, I'd like to have a bash builtin to achieve the same effect.
    – Philippe
    Dec 21, 2019 at 11:15
  • Oh, I am still sleepy - sorry for that, should read carefully.
    – Alex N.
    Dec 21, 2019 at 11:52

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .