Monday, October 26, 2009

Bash vs. Which

The Unix which command is useful for searching your $PATH to find which version of an executable is going to run. But did you know that it doesn't always tell you the truth? For instance, it doesn't tell you when the command you're asking about is hidden by a shell builtin (or function, keyword, or alias):


$ which echo
/usr/bin/echo


which found /usr/bin/echo on your path, but echo is a shell builtin (at least in bash), so that's what will get run, not the one on the path. The GNU man page for which presents workarounds that will pick up functions and aliases, but shell builtins still slip through the cracks. The situation is even weirder on Solaris, where which is a csh shell-script, that sources your .cshrc file as part of its operation -- very strange if your shell is bash.

Clearly, which is not to be trusted. The easiest workaround, if you use bash, is the type builtin:


$ type -a echo
echo is a shell builtin
echo is /bin/echo


A slightly nicer workaround is to make a bash function for which, that uses type to do the right thing. I also like which to do an ls -l where applicable. Here's what I have in my .bashrc:


if alias which > /dev/null 2>&1; then unalias which; fi

function which {
hash -r
t=`type -t $@`

if [ -z "${t%%file*}" ]; then
for p in `type -p $@`; do
ls -l ${p};
done
else
type $@;
fi
}


Now which gives you much more information:


$ which -a wish
lrwxrwxrwx 1 root root 13 Dec 23 2008 /usr/local/bin/wish -> /usr/bin/wish*
lrwxrwxrwx 1 root root 7 Jan 8 2007 /usr/bin/wish -> wish8.3*


The unalias protects you in case some systemwide .bashrc or .profile aliases which in some way (unlikely, but good hygiene anyway). The call to hash clears the hashed function list, so that any changes to your PATH are reflected. The reason for the if statement in the function, is that we certainly want to report builtins, functions, etc., but if there are none, we can use type -p to make it easier to pass the filenames to ls -l. If there are builtins or such, we just punt and give the unaltered type output.

You can pass -a to this which if you want to see all the matches for the command, in order of precedence.

Note that this has to be a function, not a script, because running the script will source your .bashrc, which might change the PATH (all PATH changes should be in .profile, but sometimes people do it in .bashrc).

Friday, October 2, 2009

Csh Stupidity

One of the great things about the Linux era, is that bash seems to be taking over as the preferred shell to use, instead of csh/tcsh. The projects at my current job began during the SunOS era, so when I started there a couple years ago I went with the flow and let the sysadmin set up my default as tcsh just like everyone else there uses. When I was getting started, no one had a reasonable .profile or .bashrc I could use, so I went with tcsh so I could use the tribal .cshrc.

In terminals, the first command I run is "bash"; most of my work is in an emacs shell window, so my .emacs says (setq shell-file-name "/bin/bash") -- actually there's an (if) in there to choose cygwin's bash if I happen to be on Windows. I do a lot of shell programming at the command line, and I simply can't live with csh-style programming.

A few days ago I was working on my .cshrc file, and I noticed how dim-witted csh's if-else handling is. The classic anti-csh diatribe is Csh Programming Considered Harmful, which is an entertaining read, but there's some insanity that isn't even covered in that worthy rant. Let me start by presenting a correct csh script:
setenv FOO hello
if ($FOO == hello) then
echo hi
else if ($FOO == goodbye) then
echo see ya
endif


Source that script, and the output is:
hi


Various typos can have effects on this script that range from annoying to silent but deadly. Here's an annoying one: put the second if on a different line from the else. The output is:
hi
else: endif not found.


Well, at least you got an error message telling you something is up, although requiring two keywords to be on the same line is a level of stupidity worthy of Tcl. What if you do the opposite, and have too many endifs? Well, the output is just "hi", with no error message. I suppose that doesn't bite too much, until later when you nest another if into the script, and it suddenly gets closed by one of your stealth extra endifs.

But here's a bad one. Take the original script. Add an else at the beginning, and an endif at the end. Seems like an else without an accompanying if should get a syntax error, but it doesn't. The extra endif -- which might have been there because we always had an extra one, but never got an error about it -- closes the weird else. And guess what? The script runs with no errors, and no output. The else is kind of a "if not true". How can this be a good thing? 

[Update 2013/11/21: Ha! Now this has actually happened to me, in almost the same way described here.  I had to add some functionality to an existing script at work.  It was a branch added near the top of an if with several branches. I mistakenly added an endif at the end of my new branch. The new functionality (and the branch above it) tested fine, no error reported, so I checked it in. A few days later someone noticed that the later branches weren't running (this script gets launched from a cron job, so the problem wasn't something that would get immediately noticed). The later branches -- starting with an else for which there was no active if -- were silently skipped. Grrrr...]

I did these experiments with csh and tcsh on RedHat. Avoid csh. Long live bash!