2

I have written bash scripts that accept a directory name as an argument. A single dot ('.') is a valid directory name, but I sometimes need to know where '.' is. The readlink and realpath commands provide a resolved path, which does not help because I need to allow for symbolic links.

For example, the resolved path to the given directory might be something like /mnt/vol_01/and/then/some, whereas the script is called with '.' where '.' is /app/then/some (a sym link which would resolve to the first path I gave).

What I have done to solve my problem is use cd and pwd in combination to provide the full path I want, and it seems to have worked OK so far.

A simplified example of a script:

DEST_DIR=$1

# Convert the given destination directory to a full path, ALLOWING 
# for symbolic links.  This is necessary in cases where '.' is 
# given as the destination directory.
DEST_DIR=$(cd $DEST_DIR && pwd -L)

# Do stuff in $DEST_DIR

My question is: is my use of cd and pwd the best way to get what I want? Or is there a better way?

jago
  • 457
  • 3
  • 7
  • 15
  • How do you mean "allow for symbolic links"? The point of realpath is to resolve any symbolic links and find the minimal raw path to the location. Describe the scenario where realpath has not worked for you, please? – Gem Taylor Feb 19 '19 at 16:25
  • what should DEST_DIR be if you run program from `/a/b/c` and DEST is given as `../d/f/../g` ? – jhnc Feb 19 '19 at 16:29
  • @GemTaylor may not be the case here, but automount points are an example where you might want `/home/jhnc` instead of `/srv1/ex1/homelinks/dept2/jhnc` , or `/usr/local` instead of `/srv2/ex4/common/arch3` – jhnc Feb 19 '19 at 16:32
  • @GemTaylor, see [How is .. (dot dot) resolved in bash when cwd is a symlink to a directory](https://unix.stackexchange.com/q/459732/264812) (and links from it) for detailed information about "physical mode" versus "logical mode" directory tracking. I'd rephrase the question as: how to obtain a clean absolute PATH based on "logical mode" directory tracking. – pjh Feb 20 '19 at 20:02
  • That helps... the command-line realpath does have a flag for logical mode. The internal API is always physical mode. Bash auto-complete is logical. It is a mess. – Gem Taylor Feb 20 '19 at 20:41
  • @GemTaylor I don't want to resolve symbolic links. I want to take advantage of them, especially in cases where a relative path to another location may be on a different device. Neither `realpath` nor `readlink` gives me what I need. For whatever reason, `realpath -L` on our systems (CentOS 7) still resolves the sym links, so I cannot use it. – jago Feb 27 '19 at 16:42
  • @jhnc I'm not sure of the specific tree you have in mind. What I tried was:
        mkdir -p /a/b/c/d/e/f/g
        cd /a/b/c
        try.sh ../c/d/e/../e/f/g    # Where "try.sh" does only my assignment and echo.
        # Result was:
        /a/b/c/d/e/f/g
        # Which was expected.
    
    – jago Feb 27 '19 at 16:52
  • Hmm... I'm not sure you can, reliably. If I have a symlink containing an absolute path to a folder then `l thatsymlink/..` I get the parent of that targetted folder, not the parent of the symlink itself. There is no way I know to represent that sensibly as a relative path from my current folder. Yes, anything before the first symlink can be tidied up, but anything after is treated by the OS as relative to the target folder. However, `cd` does not follow that pattern, but instead builds a logical path - does not resolve symlinks at all, just checks legality, so cd `symlink/..` will do nothing. – Gem Taylor Feb 27 '19 at 18:05
  • You said you want to "allow for symbolic links" and that realpath, et al, don't do what you want. So, given that a path made up of symlink and `..` segments can end up anywhere, I was trying to clarify how you would want to simplify (if at all) a path like `/a/b/c/../d/f/../g`. If you're not simplifying, why not just use "$PWD/$DEST_DIR" if DEST_DIR is not absolute? (as per the accepted answer) – jhnc Feb 27 '19 at 18:53

1 Answers1

3

If all you want to do is to make an absolute path that has minimal changes from a relative path then a simple, safe, and fast way to to it is:

[[ $dest_dir == /* ]] || dest_dir=$PWD/$dest_dir

(See Correct Bash and shell script variable capitalization for an explanation of why dest_dir is preferable to DEST_DIR.)

The code above will work even if the directory doesn't exist (yet) or if it's not possible to cd to it (e.g. because its permissions don't allow it). It may produce paths with redundant '.' components, '..' components, and redundant slashes (`/a//b', '//a/b/', ...).

If you want a minimally cleaned path (leaving symlinks unresolved), then a modified version of your original code may be a reasonable option:

dest_dir=$(cd -- "$dest_dir"/ && pwd)
  • The -- is necessary to handle directory names that begin with '-'.
  • The quotes in "$dest_dir" are necessary to handle names that contain whitespace (actually $IFS characters) or glob characters.
  • The trailing slash on "$dest_dir"/ is necessary to handle a directory whose relative name is simply -.
  • Plain pwd is sufficient because it behaves as if -L was specified by default.

Note that the code will set dest_dir to the empty string if the cd fails. You probably want to check for that before doing anything else with the variable.

Note also that $(cd ...) will create a subshell with Bash. That's good in one way because there's no need to cd back to the starting directory afterwards (which may not be possible), but it could cause a performance problem if you do it a lot (e.g. in a loop).

Finally, note that the code won't work if the directory name contains one or more trailing newlines (e.g. as created by mkdir $'dir\n'). It's possible to fix the problem (in case you really care about it), but it's messy. See How to avoid bash command substitution to remove the newline character? and shell: keep trailing newlines ('\n') in command substitution. One possible way to do it is:

dest_dir=$(cd -- "$dest_dir"/ && printf '%s.' "$PWD")    # Add a trailing '.'
dest_dir=${dest_dir%.}                                   # Remove the trailing '.'
pjh
  • 6,388
  • 2
  • 16
  • 17