Skip to content

Instantly share code, notes, and snippets.

@hrj
Last active November 29, 2023 09:50
Show Gist options
  • Save hrj/2efd3f8f9b465e01fe09 to your computer and use it in GitHub Desktop.
Save hrj/2efd3f8f9b465e01fe09 to your computer and use it in GitHub Desktop.
pwd alias hell

TL;DR

When you are in a shell in Linux, you may be led to believe you are in directory xyz but might actually be in directory pqr.


Demonstration

In a terminal,

cd /tmp
mkdir xyz
cd xyz
echo Hi from terminal 1 > myFile

Now keeping this terminal open, spawn a new terminal window and in the new terminal,

cd /tmp
mv xyz pqr

The path /tmp/xyz is not valid any more. The file /tmp/xyz/myFile has been now moved to /tmp/pqr/myFile. All fine so far, but ...

Go back to the first terminal and type:

$ pwd
/tmp/xyz
$ cat myFile
Hi from terminal 1

Whoa! What just happened?

The shell reports you are in /tmp/xyz. But when you read the file with a relative path (myFile), it actually reads /tmp/pqr/myFile.

How bad could this be?

I got severely burnt by this today, so let me provide that as an example.

I have a directory where I keep some old data (which happens to be a FoxPro database). This data is then consumed by an application written in Scala. And this setup has been stable for several months now.

But today, I acquired a new snapshot of the data. So I moved the existing directory (xyz) to a new location (pqr), and unzipped the new snapshot into my standard directory location (xyz). However, the terminal where I ran the application from, was not closed. Its pwd showed xyz, but it now picked its data from pqr.

To cut a long story short: Chaos ensued! The application was now showing old data, which I interpreted to be either an in-memory corruption or a database corruption. To fix this, I peeled away several layers of the application, and finally ended up debugging the database driver! It was only after exhausting all possibilities, that I had a brain wave to check the md5sum of the data and that's when the penny fell.

Solution

Here's a simple fix. It's for zsh, but you should be able to easily adapt it to other shells.

You just need to add these lines to your ~/.zshrc:

_pwd_alias_hell () {
  if [[ `readlink -f ./` != `readlink -f $(pwd)` ]] then;
    echo You are in pwd alias hell!
    echo "pwd        =" `pwd`
    echo "actual pwd =" `readlink -f ./`
  fi
}

autoload add-zsh-hook
add-zsh-hook precmd _pwd_alias_hell

Now whenever you run a command in the shell or simply press Enter, the above function will check the pwd and raise an alert when it has been aliased.

The above function is intentionally bare, to help you see how it works. You can style it, to make the alert message more prominent, like so:

_pwd_alias_hell () {
  if [[ `readlink -f ./` != `readlink -f $(pwd)` ]] then;
    echo $fg_bold[red]You are in pwd alias hell!$reset_color
    echo "pwd        =" $fg_bold[blue]`pwd`$reset_color
    echo "actual pwd =" $fg_bold[blue]`readlink -f ./`$reset_color
  fi
}

autoload add-zsh-hook
add-zsh-hook precmd _pwd_alias_hell

Changes

  • The solution was updated a bit. The earlier check was:
if [[ `readlink -f ./` != `pwd` ]] then;

but that threw up false negatives for symlinked directories. The new test is more robust.

@ntnn
Copy link

ntnn commented Jul 4, 2015

How bad could this be?

It would be a lot worse if ext4 wouldn't identify items in your filesystem by their inodes but by their path instead. NTFS is using paths to identify files. One particularly bad thing about this is that you can create a file x and give it certain pemissions - you move it away and create a new file x. Guess which permissions new x now has.
Yup, the ones you gave old x instead of the default.

Its pwd showed xyz, but it now picked its data from pqr.

That's a bit confusing. Are you referring to pwd (the builtin), pwd (the program), what your shell shows for %~ in your prompt or the environment variable $PWD - both $PWD and %~ are not being updated unless you change your directories[1]. pwd the zsh builtin shows the correct entry from the inode - dunno about the program pwd though. It might be wrong since it officially just reads $PWD.

1:02:07 ~temp 0%>mcd wombat
1:02:13 ~temp/wombat 0%>touch wl
1:02:16 ~temp/wombat 0%>l
total 0
-rw-rw-r-- 1 ntnn ntnn 0 Jul  5 01:02 wl
# here I opened a pane and executed `cd .. && mv wombat combat`
1:02:28 ~temp/wombat 0%>pwd
/home/ntnn/temp/combat
1:02:32 ~temp/wombat 0%>readlink -f .
/home/ntnn/temp/combat

In other words: Your fix wouldn't work - you'd need to use $PWD instead of pwd.

[1]: from the man page:
PWD The present working directory. This is set when the shell initializes and whenever the directory changes.

@alcuadrado
Copy link

Why not something like this and forget about the bug? And it won't hurt each Enter's performance.

function pwd() { cd `readlink -f .`; pwd "$@"; }

@hrj
Copy link
Author

hrj commented Jul 5, 2015

@ntnn

"pwd the zsh builtin shows the correct entry from the inode"

It doesn't for me. And I checked it is indeed the built-in pwd, with type pwd. Can you check what your type pwd says?


@alcuadrado
It's not just pwd that is affected. All "relative file-paths" get aliased, as shown in the demo example.

@dividedmind
Copy link

Fish user here. Fish builtin pwd gives me the correct answer; as does /bin/pwd. $PWD stays stale until I cd ..

Still good to know -- will make me think twice next time I use $PWD in a shell script.

@iMerica
Copy link

iMerica commented Jul 5, 2015

@alcuadrado That recurses infinitely in zsh. Even when using \pwd and /bin/pwd.

@ntnn
Copy link

ntnn commented Jul 5, 2015

@hrj Yes, it is the builtin pwd, it has been a builtin since 1999 with the last change being in 2004 - so either your filesystem is bad, your version of zsh was somehow modified or you accidentally ran pwd in the new terminal in the old, moved directory.

@hrj
Copy link
Author

hrj commented Jul 5, 2015

@ntnn It's an unmodified zsh from ubuntu. And I can reproduce the problem reliably on different filesystems, including ext4 and a custom fs mounted through FUSE. Moreover, it is not just pwd. Even cat myFile shows the problem.

Copy link

ghost commented Jul 5, 2015

Does this happen only in zsh or in other shells as well?

@grochmal
Copy link

grochmal commented Jul 5, 2015

It might be wise to change readlink -f to realpath. It might not work on old Linux installations but will work on FreeBSD (and it's variants, like PC-BSD).

BSD's readlink uses -f for another purpose. Linux uses GNU coreutils readlink, which added -f as an extension.

People struggled with that little difference between GNU and *BSD for years, until both sides agreed (more or less) to use realpath. When used without switches both GNU realpath and freeBSD realpath work in exactly the same way as GNU's readlink -f.

The only issue is that realpath was added to GNU coreutils only in 2012, and some older Linux systems might not have it (there are people running Red Hat 5 out there).

@ntnn
Copy link

ntnn commented Jul 5, 2015

@hrj As said earlier - cat myFile is correct, since the file still belongs to the inode.

Think of it as this:
Your create the folder xyz, which is represented in the filesystem as this:

inode id: inodeId1
    type: dir
    path: /path/to/xzy
    contents: [...], myFileInodeId, [...]
    [...]

Then you run mv /path/to/xyz /new/path, which changes the entry to this:

inode id: inodeId1
    type: dir
    path: /new/path
    contents: [...], myFileInodeId, [...]
    [...]

Your first terminal still holds the old path as $PWD, but is still referencing that inode with id inodeId1 - even though it now has a new path-entry. When you run cat myFile it will look for a fitting inode listed in the contents-entry of its current inode (if you run e.g. vim on the file and inside of it echo expand('%:p') to expand and echo the path it will most likely show /new/path/myFile).

I don't know why your builtin pwd is returning the old status, but I'd create an issue for that or ask the maintainer of your package about it.

@alcuadrado
Copy link

@iMerica You are right, I must have tested it on another shell :s

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment