A Couple of Shell Quickies

Since I got asked several sh-related questions, I might as well get a post out of them.

One person asks:

I’m writing a portable shell script to download a file from the web, and then compare it to a locally-stored version of the same file, using diff.

My first version of the script used mktemp to download the web-file to temporary file, run the diff command, and then delete the temporary file afterwards. e.g.

TEMPFILE=$(mktemp)
wget -q $ONLINEFILEURL -O $TEMPFILE
diff $TEMPFILE $LOCALFILE
rm $TEMPFILE

However I later discovered that the BSD version of mktemp has an incompatible syntax to the GNU version of mktemp. So then I got rid of the usage of temporary files completely, by using input-redirection, e.g.

diff <(wget -q $ONLINEFILEURL -O -) $LOCALFILE

However while this works fine under bash and ksh, it fails under ash and sh with

Syntax error: "(" unexpected

to which I replied:

The first obvious problem here is that “(wget -q $ONLINEFILEURL -O -)” isn’t a filename, it’s a subprocess. So the shell sees “<” and expects a filename, but finds “(” instead.

It looks as though the way to get diff to read from stdin is the standard way: specify “-” as the filename, and give it input on stdin. Since you’re feeding it the output from a process, you want to use a pipe:

wget -q $ONLINEFILEURL -O - | diff - $LOCALFILE

I also suggested that he could try to figure out which version of mkfile he was using:

# Wrapper function for GNU mktemp
gnu_mktemp() {
	mktemp /tmp/tmpfile.XXXXXX "$@"
}

# Wrapper function for BSD mktemp
bsd_mktemp() {
	mktemp -t /tmp/tmpfile.XXXXXX "$@"
}

# Try to figure out which wrapper to use
if mktemp -V | grep version >/dev/null 2>&1; then
	MKTEMP=gnu_mktemp
else
	MKTEMP=bsd_mktemp
fi

mytmpfile=`$MKTEMP`

And, of course, if race conditions and security aren’t a big concern, there’s always

mytmpfile=/tmp/myprogramname.$$

Another person wanted to write a bash script that would do one thing when run by him or root, and another thing if run by anyone else (basically, die with an error message about insufficient privileges and/or grooviness).

He asked whether the following two expressions were equivalent:

  • if [[ ( `whoami` != "root" ) || ( `whoami` != "coolguy" ) ]]
  • if [[ ! ( `whoami` = "root" ) || ( `whoami` = "coolguy" ) ]]

They’re not, but maybe not for obvious reasons, because propositional logic is a harsh mistress.

In the first expression,

if [[ ( `whoami` != "root" ) || ( `whoami` != "coolguy" ) ]]

let’s say that the user is joeblow. In this case, “`whoami` != "root"” is true, and so the shell can short-circuit the rest of the “||“, because the entire expression is true.

If the user is root, then the first part, “( `whoami` != "root" )” is false. However, the second part, “( `whoami` != "coolguy" )” is true (because rootcoolguy), and so the entire expression is “false || true”, which is true.

The second expression,

if [[ ! ( `whoami` = "root" ) || ( `whoami` = "coolguy" ) ]]

is closer to what he wanted, but doesn’t work because of operator precedence: “!” binds more tightly than “||“, so the expression is equivalen tto “(whoami ≠ root) || (whoami = coolguy)”.

In this case, if the user is joeblow, the first clause, “whoami ≠ root“, is true, and so the entire expression is true.

Worse yet, if the user is root, then neither the first nor second clause is true, so the entire clause is false.

What he really wanted was something like:

if [[ ( `whoami` = "root" ) || ( `whoami` = "coolguy" ) ]]; then
	# Do nothing
	:
else
	# Do something
	echo "Go away"
	exit 1
fi

Except, of course, that since the if-clause is empty, it can be cut out entirely. Then all we need to do is to negate the condition and only keep the code in the else-clause:

if [[ ! ((`whoami` = "root" ) || ( `whoami` = "coolguy" )) ]]

Note the extra pair of parentheses, to make sure that the “!” applies to the whole thing.

(Update, May 18: Fixed HTML entities.)

Bourne Shell Introspection

So I was thinking about how to refactor our custom Linux and Solaris init scripts at work. The way FreeBSD does it is to have the scripts in /etc/rc.d define variables with the commands to execute, e.g.,

start_cmd='/usr/sbin/foobard'
stop_cmd='kill `cat /var/run/foobar.pid`'

run_rc_command "$1"

where $1 is “start”, “stop”, or whatever, and run_rc_command is a function loaded from an external file. It can check whether $stop_cmd is defined, and if not, take some default action.

This is great and all, but I was wondering whether it would be possible to check whether a given shell function exists. That way, a common file could implement a generic structure for starting and stopping daemons, and the daemon-specific file could just set the specifics by defining do_start and do_stop functions.

The way to do this in Perl is to iterate over the symbol table of the package you’re looking for, and seeing whether each entry is a function. The symbol table for Foo::Bar is %Foo::Bar::; for the main package, it’s %::. Thus:

while (my ($k, $v) = each %::)
{
	if (defined())
	{
		print "$k is a functionn";
	}
}

sub test_x() {}
sub test_y() {}
sub test_z() {}

But I didn’t know how to do it in the Bourne shell.

Enter type, which tells you exactly that:

#!/bin/sh

# List of all known commands
STD_CMDS="start stop restart status verify"
MORE_CMDS="graceful something_incredibly_daemon_specific"

do_start="This is a string, not a function"

do_restart() {
	echo "I ought to restart something"
}

do_graceful() {
	echo "I am so fucking graceful"
}

for cmd in ${STD_CMDS} ${MORE_CMDS}; do
	if type "do_$cmd" >/dev/null 2>&1; then
		echo "* do_$cmd is defined"
	else
		echo "- do_$cmd is not defined"
	fi
done

And yes, this works not just in bash, but in the traditional, bourne-just-once shell, on every platform that I care about.

So yay, it turns out that the Bourne shell has more introspection than
I thought.