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 $TEMPFILEHowever 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 -) $LOCALFILEHowever 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 root ≠ coolguy), 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.)