Tuesday, July 5, 2011

Episode #152: Follow the Bouncing Link

Hal's hot on the trail

I'm a fan of the Debian "Alternatives" system. It's an elegant way of configuring which text editor, mail server, installation of Java, etc should be the default version used on the system. The only downside is that it can sometimes be non-obvious where the actual executable resides because of all of the symlinks involved. And there are plenty of other situations where the actual file you're looking for might be on the other end of a long chain of links.

In the cases of Debian's Alternatives, the update-alternatives command-line interface lets you query to find out information about the final executable path name at the end of the chain of links:

$ update-alternatives --list java

Instead of "--list", you can also use "--display" to get even more detailed information:

$ update-alternatives --display java
java - auto mode
link currently points to /usr/lib/jvm/java-6-openjdk/jre/bin/java
/usr/lib/jvm/java-6-openjdk/jre/bin/java - priority 1061
slave java.1.gz: /usr/lib/jvm/java-6-openjdk/jre/man/man1/java.1.gz
Current `best' version is /usr/lib/jvm/java-6-openjdk/jre/bin/java.

But what about cases where you have a chain of symlinks that aren't part of the alternatives system? "Friend of the Blog" Jeff Haemer sent along this nice little snippet of Fu:

$ readlink -f $(which java)

"which java" returns the cached executable path from our search path. Normally I'd prefer using the "type" command instead of "which" because type will even tell you if the command you're executing is actually an alias. But in this case, "which" is better because it simply returns the executable path while "type" adds some extra text:

$ type java
java is /usr/bin/java
$ which java

We take the executable path returned by "which" and use it as an argument to "readlink -f". "readlink" will tell you what file a given link points to, and "-f" will follow an entire chain of symlinks and tell you the final path that the last link in the chain points to.

There's only one small problem: "readlink" is part of the GNU coreutils package and may not be available on all flavors of Unix. I can do a simple version of "readlink" that only dereferences a single layer of symlinks with the following Fu:

$ ls -l $(which java) | awk '/->/ {print $NF}'

The "ls" output about our symlink is going to have a "->" symbol followed by the path that the link points to, which will also be the last thing on the line. So I use awk to match the "->" and then print out the last field, aka "$NF".

To emulate "readlink -f", I'm going to need a loop:

$ exec=$(which java)
$ while [[ -L $exec ]]; do exec=$(ls -l $exec | awk '/->/ {print $NF}'); done
$ echo $exec

First I set the variable "$exec" to be the path name returned by "which java". Then as long as my "$exec" path is a symlink ("[[ -L $exec ]]") I use my "readlink" stand-in to set the new value of "$exec" to be the path that the link points to. Eventually the last link in the chain will point to an actual file and the loop will terminate. At that point, I just output the last value of "$exec".

With a little more Fu, we can actually see each step of the process:

$ exec=$(which java)
$ while [[ -L $exec ]]; do
link=$(ls -l $exec | awk '/->/ {print $NF}');
echo $exec points to $link;

/usr/bin/java points to /etc/alternatives/java
/etc/alternatives/java points to /usr/lib/jvm/java-6-openjdk/jre/bin/java

The loop is essentially the same. I've just added an extra variable so I can print out both the link name in "$exec" and the thing it points to in "$link". The extra output may be useful if you're ever trying to debug what's going wrong with a long chain of symlinks.

Now I know that Windows doesn't have anything like the Alternatives system in Debian, but maybe Tim has some command-line magic up his sleeve for decoding all of those nasty shortcut files that users like to set up?

Tim is trailing

We don't have all that fancy mumbo-jumbo on the Windows side. We pretty much just have Links (a.k.a. Shortcuts). These Link files have the extension "lnk". When viewing them via the GUI you won't see the ".lnk" extension, but via the command line you do.

PS C:\Users\tim\Desktop> ls *.lnk 

Directory: C:\Users\tim\Desktop

Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 7/4/2011 07:04 PM 893 TweetDeck.lnk

If you use Get-Content (alias gc, cat, type) to view the file it is mostly unprintable characters, but it does contain the path in the file. We don't have a command similar to the *nix "strings" command so we can't extract it that way. However, we can use some weird stuff built-in in tools to extract the path from the file.

PS C:\> $s = New-Object -ComObject WScript.Shell
PS C:\> $s.CreateShortcut('C:\Users\tim\Desktop\TweetDeck.lnk').TargetPath
C:\Program Files\TweetDeck\TweetDeck.exe

Yep, its really that ugly.

We start off by creating a new Windows Shell Object. Once we create the object we use the CreateShortcut method to open an existing shortcut. At first glance you might think we are creating a shortcut, but we aren't. The method's name is a bit of a misnomer as it can be used to create or open an existing shortcut.

Once the shortcut is open, we then output the value of the TargetPath property. One additional point of weirdness, the path given to CreateShortcut must be the full path. If you give it a relative path, including just the filename, it returns nothing since it can't find the shortcut.

I wouldn't exactly call it magic, but it does work.