Categories
bash

Oppa Semicolon Style

[No relation to that infuriatingly viral video.]

A recent Reddit thread about ffmpeg encoding somehow sidetracked into an unrelated discussion about semicolons. In the process of answering the questions therein, I suddenly realized that most bash newbies get confused about semicolons in scripts, and that there’s in fact a striking similarity between compound statements in bash and C.

First, a simple fact: Semicolons punctuate compound statements, and punctuation promotes proper parsing. Just look at the descriptions of compound statements in the bash man page:

case word in [ [(] pattern [ | pattern ] ... ) list ;; ] ... esac

if list; then list; [ elif list; then list; ] ... [ else list; ] fi

for name [ [ in [ word ... ] ] ; ] do list ; done

and think about how any program could be expected to assume that:

for i in 1 2 3 do echo do you do this $i time\? done

actually means:

for i in 1 2 3; do echo do you do this $i time\?; done

as opposed to the programmer having a complete brain-fart.

Now, you’re probably going: “Hang on, I don’t see anyone writing code all in a line like that!” That leads me to my next point: Semicolons can generally be replaced by newlines in compound statements. Hence, you’re far more likely to see this:

for i in 1 2 3; do
  echo $i
  echo breaker
done

or this:

for i in 1 2 3
do
  echo $i
  echo breaker
done

Notice that the first form simply substitutes a newline for the semicolon before done, and the second form also substitutes a newline for the semicolon before do. Both are functionally identical to each other and to the single-line (note the newline-semicolon substitution between the two echo statements as well):

for i in 1 2 3; do echo $i; echo breaker; done

as well as the odd duck:

for i in 1 2 3
do
          echo $i
  echo breaker; done

Don’t get crazy, though. The following is actually illegal in bash:

for i in 1
  2
     3
do
  echo $i; echo
    breaker; done

because it translates to the incorrect one-liner:

for i in 1; 2; 3; do echo $i; echo; breaker; done

“Wait a sec!” I hear you call, dear reader. “How about the double-semicolon in case clauses?”

Technically, bash treats this double-semicolon as a single token, so you can’t replace it with two newlines (or one, for that matter). You also can’t write:

case $i in
(1) echo yes ; ;
esac

That space between the two semicolons will cause bash to choke.


Now, if you’re a C programmer, you probably recognized the “shape” of the first two examples as being the equivalent of the indentation styles1 popularly known as K&R:

while (1) {
  do(something);
}

and Allman:

while (1)
{
  do(something);
}

I’ll therefore call the equivalents in bash K&R-style and Allman-style. Which one to use is purely a matter of preference; pick one and stick with it.

As for the all-in-one-liner, I’ll just call it No-style. You should avoid this style wherever possible, as it gets seriously unreadable very quickly. You’ll also regret it when you get hit by a dozen syntax errors on line 1; good luck trying to find them all.

And if you were working for me, and showed me that odd-duck code chunk…let’s just say you’d no longer be working for me. Call it NoJob-style.


  1. See Indentation Style – Wikipedia for more styles than you should ever care about. ↩︎
Categories
bash

$SHELL: The $0-sum Game

Q: Which shell am I currently running?
A: $SHELL.

NOPE.

The SHELL environment variable has a very specific meaning. Here’s what the POSIX standard has to say about it:

SHELL
This variable shall represent a pathname of the user’s preferred command language interpreter.

For every shell I’ve ever used, that highlighted phrase has been interpreted as the user’s login shell, which may not be relevant to the currently-running environment. For instance, cron specifically forces $SHELL in all cron jobs to /bin/sh by default, ostensibly for sanity and/or security reasons.

Also:

~ $ echo $SHELL
/bin/bash
~ $ zsh
~ % echo $SHELL
/bin/bash

In everyday scripting, $SHELL should only ever be used to answer one question: “What shell should my process spawn if necessary?

And so we return to the original question…

Q: Which shell am I currently running?
A: $0 from the command line.

~ $ echo $0
-bash
~ $ zsh
~ % echo $0
zsh

But I want to know what shell is running my script in the script itself. How do I do that?

On Linux, you could run this code snippet (credit: Evan Benn):

sh -c 'ps -p $$ -o ppid=' | xargs -I'{}' readlink -f '/proc/{}/exe'

But a much better way of going about it is to first ask yourself why you need to know. If, as is almost always the case, you want to do something that’s shell-specific, then simply test for a variable that’s unique to your desired shell (e.g. $BASH, $ZSH_NAME), then execute the desired code accordingly. Easy peasy.