4

I noticed different .sh scripts for homebrew which check whether it's installed.

One uses this syntax:

if test ! $(which brew); then

The other uses this syntax:

if ! which brew > /dev/null; then

I get that they are both checking the exit code for which brew, but I'm curious as to whether there are other differences between them. To me, the former is clearer, and I would assume (maybe incorrectly) more efficient since it doesn't have to redirect any output.

Also, why does the second actually execute which brew, regardless of whether the output is sent to /dev/null or not? Just how shell works?

2 Answers2

4

The first is a clumsy hack, the second is a common mistake.

These two tests do something completely different, which happens to produce the same outcome.

if test ! $(which brew); then

This tests whether the output of which brew is empty.

  • If brew is on the search path, then which brew produces a word of output, so the test command receives two arguments: ! and the path to brew. When test has two arguments, the first of which is !, it returns true if the second argument is empty (which isn't the case here) and false otherwise.
  • If brew isn't on the search path, then the output of which brew is empty, so the test command receives a single argument which is !, so test returns true.

Note that this command will produce an error message and a failure status if the path to brew contains whitespace, because that's what an unquoted command substitution means. It so happens that a failure status was the desired outcome here, so this works in a roundabout way.

This command does not test the exit code of which brew. The exit code is ignored.

if ! which brew > /dev/null; then

This is the straightforward way to test if which brew succeeds. It doesn't depend on anything brittle except which itself.

which brew is always called in both cases. Why would it matter that the output is redirected to /dev/null? “Hide this command's output” does not mean “don't run this command”.

The proper way to test if brew is not available on the command search path is

if ! type brew >/dev/null 2>/dev/null; then

See Why not use "which"? What to use then?

  • this makes sense - after reading the linked question, it seems the only case in which 'which' might fail is if there's an alias or function in ~/.bashrc or ~/.cshrc

    my question re: output redirection...thinking abstractly, if I said to you 'If you run around a mile, I'll give you $10' doesn't actually cause you to run a mile. And in other coding languages I'm familiar with, 'if' statements don't actually execute the conditional statement. I'm just curious why the statement is actually executed here!

    – runofthemill Apr 17 '16 at 00:20
1

Part one if test ! $(which brew); then ...

  1. If you run which brew your $PATH is searched for an executable called brew. If one is found the name is printed to stdout. If nothing was found an error message is printed to stderr. (Some shells provide a builtin which that also reports builtins and shell functions, reserved words, .... For example zsh.)
  2. $(...) captures the stdout (but not the stderr) of whatever the ... shellcommands are and puts it on the command line as argument(s).
  3. test ! ... returns the inverted return code of test .... That is test's way of negating an expression. test ... (without any options in the ... part) just tests if the ... part is not the empty string.

Result: which is used to look for an executable called brew. The resulting path or an empty string is put on the command line as an argument to test ! which checks if this argument is the empty string and thus returns 0 (true).

Part two if ! which brew > /dev/null; then ...

Here which brew is the same as above. If which finds an executable it not only prints the name but also returns with code 0 (true). If it doesn't find anything it prints an error message and returns some other code (false). This code is inverted by the shell because of the ! in front of the command (0->1, anything else -> 0). That is what if cares about. Because the user does not care to see the actual path of brew the standard output of which is redirected to /dev/null.

Notes:

In both cases the stderr of which is not redirected and should be visable on the terminal.

I did not test it but I assume that the second form is faster as it starts only one process/builtin. But both commands do redirection: The second one explicitly to /dev/null, the first one less obvious (at first) by capturing the output of which with $(...). You can roughly compare the speed with something like the comp function I use.

There are man pages for which and test. And the man page of the shell (sh, ksh, bash, zsh) has a section about redirection and command substitution, as well as on builtins (not only which but also test is often implemented as a builtin).

Lucas
  • 2,845